pkgs/bot/src/server/discord.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
import config from "@/config";
import { attachments, ticketMessages, type Ticket } from "@/database/schema";
import client from "@/discord";
import { intoApiAttachment, marshallIntoApiUser } from "./api";
import _logger from "@/utils/logging";
import type { User as UserProfile, Ticket as ApiTicket } from "@stealth-developers/api";
import { asc, eq, or, and, inArray } from "drizzle-orm";
import { db } from "@/database";
const logger = _logger.child({ name: "server" });
export const cache = new Map<string, UserProfile | null>();
export async function getUsers(ids: (string | null | undefined)[]): Promise<void> {
const uniqueIds = [...new Set(ids.filter(Boolean) as string[])];
const missingIds = uniqueIds.filter((id) => id !== "system" && !cache.has(id));
if (missingIds.length === 0) return;
logger.debug(missingIds, `fetching ${missingIds.length} users`);
const guild = client.guilds.cache.get(config.discord.guild);
if (guild) {
try {
const members = await guild.members.fetch({ user: missingIds });
members.forEach((member) => cache.set(member.id, marshallIntoApiUser(member)));
} catch (err) {
logger.error({ err }, "failed to fetch guild members chunk");
}
}
const stillMissing = missingIds.filter((id) => !cache.has(id));
if (stillMissing.length > 0) {
await Promise.all(
stillMissing.map(async (id) => {
try {
const user = await client.users.fetch(id);
cache.set(id, marshallIntoApiUser(user));
} catch {
cache.set(id, null);
}
}),
);
}
}
export async function getUser(id: string): Promise<UserProfile | null> {
await getUsers([id]);
return cache.get(id) || null;
}
export async function hydrateTickets(allTickets: Ticket[]): Promise<ApiTicket[]> {
const allIds = new Set<string>();
for (const ticket of allTickets) {
if (ticket.authorId) allIds.add(ticket.authorId);
if (ticket.openedBy) allIds.add(ticket.openedBy);
if (ticket.closedBy && ticket.closedBy !== "reporter") allIds.add(ticket.closedBy);
}
await getUsers([...allIds]);
const promises = allTickets.map(async (ticket) => {
const fallbackSubject: UserProfile = {
id: ticket.authorId,
name: "Subject",
avatarUrl: "",
isModerator: false,
};
const messages = await db
.select()
.from(ticketMessages)
.where(eq(ticketMessages.ticketId, ticket.id))
.orderBy(asc(ticketMessages.createdAt));
const messageIds = messages.map((m) => m.id);
const ticketAttachments = await db
.select()
.from(attachments)
.where(
or(
and(eq(attachments.ownerType, "ticket"), eq(attachments.ownerId, ticket.id)),
messageIds.length > 0
? and(
eq(attachments.ownerType, "ticket_message"),
inArray(attachments.ownerId, messageIds),
)
: undefined,
),
);
const ticketLevelAttachments = ticketAttachments.filter(
(a) => a.ownerType === "ticket" && a.ownerId === ticket.id,
);
return {
number: String(ticket.id),
status: ticket.status as "open" | "closed" | "archived",
closedBy: ticket.closedBy ? (cache.get(ticket.closedBy) ?? null) : null,
closedAt: ticket.closedAt?.toUTCString() || null,
topic: ticket.topic,
closeReason: ticket.closeReason,
privateReason: ticket.privateReason,
openedAt: ticket.createdAt?.toUTCString(),
openedBy: ticket.openedBy ? (cache.get(ticket.openedBy) ?? null) : null,
subject: cache.get(ticket.authorId) ?? fallbackSubject,
attachments: await Promise.all(ticketLevelAttachments.map(intoApiAttachment)),
} satisfies ApiTicket;
});
return Promise.all(promises);
}
export function notify(channelId: string, message: string) {
const channel = client.channels.cache.get(channelId);
if (!channel) return Promise.resolve();
if (channel.isSendable())
channel.send({
content: message,
allowedMentions: { parse: [] },
});
}
|