fix: wire DM file attachments through the full send/fetch/broadcast chain
DM uploads silently failed because the 5-point chain (types, frontend, POST, GET, WS broadcast) was never wired for attachments. Added buildDmMessageWithUser/getDmMessageWithUser helpers mirroring the server channel pattern, and plumbed attachmentIds + replyToId through all DM code paths.
This commit is contained in:
@@ -14,6 +14,8 @@ import type {
|
|||||||
CreateDmMessageRequest,
|
CreateDmMessageRequest,
|
||||||
AddDmMemberRequest,
|
AddDmMemberRequest,
|
||||||
PaginatedQuery,
|
PaginatedQuery,
|
||||||
|
Attachment,
|
||||||
|
Reaction,
|
||||||
} from '@opencord/shared';
|
} from '@opencord/shared';
|
||||||
|
|
||||||
function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
||||||
@@ -28,6 +30,121 @@ function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch-fetch reactions for a set of DM message IDs.
|
||||||
|
* Returns a map from dmMessageId to Reaction[].
|
||||||
|
*/
|
||||||
|
export function fetchDmReactionsForMessages(dmMessageIds: string[]): Map<string, Reaction[]> {
|
||||||
|
if (dmMessageIds.length === 0) return new Map();
|
||||||
|
const db = getDb();
|
||||||
|
const reactionRows = db.select()
|
||||||
|
.from(schema.dmReactions)
|
||||||
|
.where(inArray(schema.dmReactions.dmMessageId, dmMessageIds))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
// Batch fetch users for reactions
|
||||||
|
const reactionUserIds = [...new Set(reactionRows.map(r => r.userId))];
|
||||||
|
const reactionUsers = reactionUserIds.length > 0
|
||||||
|
? db.select().from(schema.users).where(inArray(schema.users.id, reactionUserIds)).all()
|
||||||
|
: [];
|
||||||
|
const reactionUserMap = new Map(reactionUsers.map(u => [u.id, u]));
|
||||||
|
|
||||||
|
const map = new Map<string, Reaction[]>();
|
||||||
|
for (const r of reactionRows) {
|
||||||
|
const user = reactionUserMap.get(r.userId);
|
||||||
|
const reaction: Reaction = {
|
||||||
|
id: r.id,
|
||||||
|
messageId: r.dmMessageId,
|
||||||
|
userId: r.userId,
|
||||||
|
emoji: r.emoji,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
user: user ? sanitizeUser(user) : undefined,
|
||||||
|
};
|
||||||
|
if (!map.has(r.dmMessageId)) {
|
||||||
|
map.set(r.dmMessageId, []);
|
||||||
|
}
|
||||||
|
map.get(r.dmMessageId)!.push(reaction);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure transformer: builds a DmMessageWithUser from pre-fetched data.
|
||||||
|
*/
|
||||||
|
export function buildDmMessageWithUser(
|
||||||
|
message: typeof schema.dmMessages.$inferSelect,
|
||||||
|
user: typeof schema.users.$inferSelect,
|
||||||
|
attachmentRows: (typeof schema.attachments.$inferSelect)[],
|
||||||
|
reactions: Reaction[] = [],
|
||||||
|
replyTo: DmMessageWithUser | null = null,
|
||||||
|
): DmMessageWithUser {
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
dmChannelId: message.dmChannelId,
|
||||||
|
userId: message.userId,
|
||||||
|
replyToId: message.replyToId,
|
||||||
|
content: message.content,
|
||||||
|
editedAt: message.editedAt,
|
||||||
|
createdAt: message.createdAt,
|
||||||
|
user: sanitizeUser(user),
|
||||||
|
attachments: attachmentRows.map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
messageId: a.dmMessageId ?? message.id,
|
||||||
|
filename: a.filename,
|
||||||
|
originalName: a.originalName,
|
||||||
|
mimetype: a.mimetype,
|
||||||
|
size: a.size,
|
||||||
|
createdAt: a.createdAt,
|
||||||
|
})),
|
||||||
|
reactions,
|
||||||
|
replyTo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a DM message by ID and hydrates it with user, attachments, reactions, and replyTo.
|
||||||
|
*/
|
||||||
|
export function getDmMessageWithUser(dmMessageId: string): DmMessageWithUser | null {
|
||||||
|
const db = getDb();
|
||||||
|
const message = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, dmMessageId)).get();
|
||||||
|
if (!message) return null;
|
||||||
|
|
||||||
|
const user = db.select().from(schema.users).where(eq(schema.users.id, message.userId)).get();
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const attachmentRows = db.select()
|
||||||
|
.from(schema.attachments)
|
||||||
|
.where(eq(schema.attachments.dmMessageId, dmMessageId))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const reactionsMap = fetchDmReactionsForMessages([dmMessageId]);
|
||||||
|
const reactions = reactionsMap.get(dmMessageId) ?? [];
|
||||||
|
|
||||||
|
let replyTo: DmMessageWithUser | null = null;
|
||||||
|
if (message.replyToId) {
|
||||||
|
const replyMsg = db.select().from(schema.dmMessages).where(eq(schema.dmMessages.id, message.replyToId)).get();
|
||||||
|
if (replyMsg) {
|
||||||
|
const replyUser = db.select().from(schema.users).where(eq(schema.users.id, replyMsg.userId)).get();
|
||||||
|
if (replyUser) {
|
||||||
|
replyTo = {
|
||||||
|
id: replyMsg.id,
|
||||||
|
dmChannelId: replyMsg.dmChannelId,
|
||||||
|
userId: replyMsg.userId,
|
||||||
|
replyToId: replyMsg.replyToId,
|
||||||
|
content: replyMsg.content,
|
||||||
|
editedAt: replyMsg.editedAt,
|
||||||
|
createdAt: replyMsg.createdAt,
|
||||||
|
user: sanitizeUser(replyUser),
|
||||||
|
attachments: [],
|
||||||
|
reactions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildDmMessageWithUser(message, user, attachmentRows, reactions, replyTo);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts a DM message to all members of a DM channel.
|
* Broadcasts a DM message to all members of a DM channel.
|
||||||
* For members who have closed the channel (closed=1), also sends a
|
* For members who have closed the channel (closed=1), also sends a
|
||||||
@@ -590,18 +707,64 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const users = db.select().from(schema.users).where(inArray(schema.users.id, userIds)).all();
|
const users = db.select().from(schema.users).where(inArray(schema.users.id, userIds)).all();
|
||||||
const userMap = new Map(users.map(u => [u.id, u]));
|
const userMap = new Map(users.map(u => [u.id, u]));
|
||||||
|
|
||||||
|
// Batch fetch attachments by dmMessageId
|
||||||
|
const messageIds = messageRows.map(m => m.id);
|
||||||
|
const allAttachments = db.select()
|
||||||
|
.from(schema.attachments)
|
||||||
|
.where(inArray(schema.attachments.dmMessageId, messageIds))
|
||||||
|
.all();
|
||||||
|
const attachmentMap = new Map<string, (typeof schema.attachments.$inferSelect)[]>();
|
||||||
|
for (const att of allAttachments) {
|
||||||
|
const mid = att.dmMessageId ?? '';
|
||||||
|
if (!attachmentMap.has(mid)) attachmentMap.set(mid, []);
|
||||||
|
attachmentMap.get(mid)!.push(att);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch fetch reactions
|
||||||
|
const reactionsMap = fetchDmReactionsForMessages(messageIds);
|
||||||
|
|
||||||
|
// Batch fetch reply-to messages
|
||||||
|
const replyToIds = messageRows
|
||||||
|
.map(m => m.replyToId)
|
||||||
|
.filter((id): id is string => id !== null && id !== undefined);
|
||||||
|
const uniqueReplyIds = [...new Set(replyToIds)];
|
||||||
|
const replyToMap = new Map<string, DmMessageWithUser>();
|
||||||
|
if (uniqueReplyIds.length > 0) {
|
||||||
|
const replyMessages = db.select()
|
||||||
|
.from(schema.dmMessages)
|
||||||
|
.where(inArray(schema.dmMessages.id, uniqueReplyIds))
|
||||||
|
.all();
|
||||||
|
const replyUserIds = [...new Set(replyMessages.map(m => m.userId))];
|
||||||
|
const replyUsers = replyUserIds.length > 0
|
||||||
|
? db.select().from(schema.users).where(inArray(schema.users.id, replyUserIds)).all()
|
||||||
|
: [];
|
||||||
|
const replyUserMap = new Map(replyUsers.map(u => [u.id, u]));
|
||||||
|
|
||||||
|
for (const rm of replyMessages) {
|
||||||
|
const rUser = replyUserMap.get(rm.userId);
|
||||||
|
if (!rUser) continue;
|
||||||
|
replyToMap.set(rm.id, {
|
||||||
|
id: rm.id,
|
||||||
|
dmChannelId: rm.dmChannelId,
|
||||||
|
userId: rm.userId,
|
||||||
|
replyToId: rm.replyToId,
|
||||||
|
content: rm.content,
|
||||||
|
editedAt: rm.editedAt,
|
||||||
|
createdAt: rm.createdAt,
|
||||||
|
user: sanitizeUser(rUser),
|
||||||
|
attachments: [],
|
||||||
|
reactions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const messages: DmMessageWithUser[] = messageRows
|
const messages: DmMessageWithUser[] = messageRows
|
||||||
.map(m => {
|
.map(m => {
|
||||||
const user = userMap.get(m.userId);
|
const user = userMap.get(m.userId);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return {
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
id: m.id,
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
dmChannelId: m.dmChannelId,
|
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
||||||
userId: m.userId,
|
|
||||||
content: m.content,
|
|
||||||
createdAt: m.createdAt,
|
|
||||||
user: sanitizeUser(user),
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.filter((m): m is DmMessageWithUser => m !== null);
|
.filter((m): m is DmMessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -613,14 +776,15 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
preHandler: authenticate,
|
preHandler: authenticate,
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { content } = request.body;
|
const { content, attachments: attachmentIds, replyToId } = request.body;
|
||||||
|
|
||||||
if (!isDmMember(id, request.userId)) {
|
if (!isDmMember(id, request.userId)) {
|
||||||
return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 });
|
return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
if ((!content || typeof content !== 'string' || content.trim().length === 0) &&
|
||||||
return reply.code(400).send({ error: 'Message content is required', statusCode: 400 });
|
(!attachmentIds || attachmentIds.length === 0)) {
|
||||||
|
return reply.code(400).send({ error: 'Message must have content or attachments', statusCode: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
@@ -631,23 +795,25 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
dmChannelId: id,
|
dmChannelId: id,
|
||||||
userId: request.userId,
|
userId: request.userId,
|
||||||
content: content.trim(),
|
replyToId: replyToId || null,
|
||||||
|
content: content?.trim() || null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
const user = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
// Link attachments to this DM message
|
||||||
if (!user) {
|
if (attachmentIds && attachmentIds.length > 0) {
|
||||||
return reply.code(500).send({ error: 'User not found', statusCode: 500 });
|
for (const attId of attachmentIds) {
|
||||||
|
db.update(schema.attachments)
|
||||||
|
.set({ dmMessageId: messageId })
|
||||||
|
.where(eq(schema.attachments.id, attId))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message: DmMessageWithUser = {
|
const message = getDmMessageWithUser(messageId);
|
||||||
id: messageId,
|
if (!message) {
|
||||||
dmChannelId: id,
|
return reply.code(500).send({ error: 'Failed to create message', statusCode: 500 });
|
||||||
userId: request.userId,
|
}
|
||||||
content: content.trim(),
|
|
||||||
createdAt: now,
|
|
||||||
user: sanitizeUser(user),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Broadcast to all DM members (including those who closed the channel)
|
// Broadcast to all DM members (including those who closed the channel)
|
||||||
broadcastDmMessage(id, message);
|
broadcastDmMessage(id, message);
|
||||||
@@ -683,21 +849,11 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
.where(eq(schema.dmMessages.id, id))
|
.where(eq(schema.dmMessages.id, id))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const user = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
const updated = getDmMessageWithUser(id);
|
||||||
if (!user) {
|
if (!updated) {
|
||||||
return reply.code(500).send({ error: 'User not found', statusCode: 500 });
|
return reply.code(500).send({ error: 'Failed to update message', statusCode: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated: DmMessageWithUser = {
|
|
||||||
id: msg.id,
|
|
||||||
dmChannelId: msg.dmChannelId,
|
|
||||||
userId: msg.userId,
|
|
||||||
content: content.trim(),
|
|
||||||
editedAt: now,
|
|
||||||
createdAt: msg.createdAt,
|
|
||||||
user: sanitizeUser(user),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Broadcast to all DM members
|
// Broadcast to all DM members
|
||||||
const dmMembers = db.select()
|
const dmMembers = db.select()
|
||||||
.from(schema.dmMembers)
|
.from(schema.dmMembers)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { generateSnowflake } from '../utils/snowflake.js';
|
|||||||
import { connectionManager } from './handler.js';
|
import { connectionManager } from './handler.js';
|
||||||
import type { VoiceRoom, DmRoomMeta, ServerRoomMeta } from './handler.js';
|
import type { VoiceRoom, DmRoomMeta, ServerRoomMeta } from './handler.js';
|
||||||
import { isMember, getChannelServerId, isDmMember } from '../utils/permissions.js';
|
import { isMember, getChannelServerId, isDmMember } from '../utils/permissions.js';
|
||||||
import { broadcastDmMessage } from '../routes/dm.js';
|
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js';
|
||||||
import type { User, MessageWithUser, Attachment, DmMessageWithUser } from '@opencord/shared';
|
import type { User, MessageWithUser, Attachment, DmMessageWithUser } from '@opencord/shared';
|
||||||
|
|
||||||
function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
function sanitizeUser(row: typeof schema.users.$inferSelect): User {
|
||||||
@@ -523,15 +523,20 @@ function handleVoiceStatus(event: Record<string, unknown>, userId: string): void
|
|||||||
|
|
||||||
function handleDmMessageCreate(event: Record<string, unknown>, userId: string): void {
|
function handleDmMessageCreate(event: Record<string, unknown>, userId: string): void {
|
||||||
const dmChannelId = event.dmChannelId as string;
|
const dmChannelId = event.dmChannelId as string;
|
||||||
const content = event.content as string;
|
const content = event.content as string | undefined;
|
||||||
|
const attachmentIds = event.attachments as string[] | undefined;
|
||||||
|
const replyToId = event.replyToId as string | undefined;
|
||||||
|
|
||||||
if (!dmChannelId || typeof dmChannelId !== 'string') {
|
if (!dmChannelId || typeof dmChannelId !== 'string') {
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
|
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
const hasContent = content && typeof content === 'string' && content.trim().length > 0;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'content is required' });
|
const hasAttachments = attachmentIds && Array.isArray(attachmentIds) && attachmentIds.length > 0;
|
||||||
|
|
||||||
|
if (!hasContent && !hasAttachments) {
|
||||||
|
connectionManager.sendToUser(userId, { type: 'error', message: 'Message must have content or attachments' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,21 +553,23 @@ function handleDmMessageCreate(event: Record<string, unknown>, userId: string):
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
dmChannelId,
|
dmChannelId,
|
||||||
userId,
|
userId,
|
||||||
content: content.trim(),
|
replyToId: replyToId || null,
|
||||||
|
content: hasContent ? content.trim() : null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
const user = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
|
// Link attachments to this DM message
|
||||||
if (!user) return;
|
if (hasAttachments) {
|
||||||
|
for (const attId of attachmentIds) {
|
||||||
|
db.update(schema.attachments)
|
||||||
|
.set({ dmMessageId: messageId })
|
||||||
|
.where(eq(schema.attachments.id, attId))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dmMessage: DmMessageWithUser = {
|
const dmMessage = getDmMessageWithUser(messageId);
|
||||||
id: messageId,
|
if (!dmMessage) return;
|
||||||
dmChannelId,
|
|
||||||
userId,
|
|
||||||
content: content.trim(),
|
|
||||||
createdAt: now,
|
|
||||||
user: sanitizeUser(user),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Broadcast to all DM members (including those who closed the channel)
|
// Broadcast to all DM members (including those who closed the channel)
|
||||||
broadcastDmMessage(dmChannelId, dmMessage);
|
broadcastDmMessage(dmChannelId, dmMessage);
|
||||||
@@ -637,18 +644,8 @@ function handleDmMessageEdit(event: Record<string, unknown>, userId: string): vo
|
|||||||
.where(eq(schema.dmMessages.id, messageId))
|
.where(eq(schema.dmMessages.id, messageId))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const user = db.select().from(schema.users).where(eq(schema.users.id, userId)).get();
|
const updated = getDmMessageWithUser(messageId);
|
||||||
if (!user) return;
|
if (!updated) return;
|
||||||
|
|
||||||
const updated: DmMessageWithUser = {
|
|
||||||
id: msg.id,
|
|
||||||
dmChannelId: msg.dmChannelId,
|
|
||||||
userId: msg.userId,
|
|
||||||
content: content.trim(),
|
|
||||||
editedAt: now,
|
|
||||||
createdAt: msg.createdAt,
|
|
||||||
user: sanitizeUser(user),
|
|
||||||
};
|
|
||||||
|
|
||||||
const dmMembers = db.select()
|
const dmMembers = db.select()
|
||||||
.from(schema.dmMembers)
|
.from(schema.dmMembers)
|
||||||
|
|||||||
@@ -168,9 +168,9 @@ export interface DmMessage {
|
|||||||
|
|
||||||
export interface DmMessageWithUser extends DmMessage {
|
export interface DmMessageWithUser extends DmMessage {
|
||||||
user: User;
|
user: User;
|
||||||
attachments?: Attachment[];
|
attachments: Attachment[];
|
||||||
reactions?: Reaction[];
|
reactions: Reaction[];
|
||||||
replyTo?: MessageWithUser | null;
|
replyTo?: DmMessageWithUser | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── WebSocket Event Types ──────────────────────────────────────────────────
|
// ─── WebSocket Event Types ──────────────────────────────────────────────────
|
||||||
@@ -185,7 +185,7 @@ export type ClientEvent =
|
|||||||
| { type: 'presence_update'; status: 'online' | 'idle' | 'dnd' }
|
| { type: 'presence_update'; status: 'online' | 'idle' | 'dnd' }
|
||||||
| { type: 'voice_join'; channelId: string }
|
| { type: 'voice_join'; channelId: string }
|
||||||
| { type: 'voice_leave' }
|
| { type: 'voice_leave' }
|
||||||
| { type: 'dm_message_create'; dmChannelId: string; content: string; replyToId?: string }
|
| { type: 'dm_message_create'; dmChannelId: string; content?: string; attachments?: string[]; replyToId?: string }
|
||||||
| { type: 'dm_typing_start'; dmChannelId: string }
|
| { type: 'dm_typing_start'; dmChannelId: string }
|
||||||
| { type: 'dm_message_edit'; messageId: string; content: string }
|
| { type: 'dm_message_edit'; messageId: string; content: string }
|
||||||
| { type: 'dm_message_delete'; messageId: string }
|
| { type: 'dm_message_delete'; messageId: string }
|
||||||
@@ -319,7 +319,9 @@ export interface AddDmMemberRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateDmMessageRequest {
|
export interface CreateDmMessageRequest {
|
||||||
content: string;
|
content?: string;
|
||||||
|
attachments?: string[];
|
||||||
|
replyToId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedQuery {
|
export interface PaginatedQuery {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isDm) {
|
if (isDm) {
|
||||||
await api.dm.sendMessage(channelId, { content });
|
await api.dm.sendMessage(channelId, { content, attachments: attachmentIds, replyToId });
|
||||||
} else {
|
} else {
|
||||||
await api.channels.sendMessage(channelId, { content, attachments: attachmentIds, replyToId });
|
await api.channels.sendMessage(channelId, { content, attachments: attachmentIds, replyToId });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user