all repos — stealth-developers @ b30621f89da1d2cbca3735c0662931f8f0608758

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
import { type APIGuildMember, type APIUser } from "discord-api-types/v10";
import type { GuildMember, User } from "discord.js";

import config from "@/config";
import type { Ticket } from "@/database/schema";
import client from "@/discord";

import _logger from "@/utils/logging";
const logger = _logger.child({ name: "server" });

export type UserProfile = APIGuildMember | { user: APIUser };

export const cache = new Map<string, UserProfile | null>();

function mapUser(user: User): APIUser {
	return {
		id: user.id,
		username: user.username,
		discriminator: user.discriminator,
		global_name: user.globalName,
		avatar: user.avatar,
		bot: user.bot,
		system: user.system,
		banner: user.banner,
		accent_color: user.accentColor,
		public_flags: user.flags?.bitfield,
	} as APIUser;
}

function mapMember(member: GuildMember): APIGuildMember {
	return {
		user: mapUser(member.user),
		nick: member.nickname,
		avatar: member.avatar,
		roles: Array.from(member.roles.cache.keys()),
		joined_at: member.joinedAt?.toISOString() || new Date().toISOString(),
		deaf: member.voice.serverDeaf || false,
		mute: member.voice.serverMute || false,
		flags: member.flags.bitfield,
		pending: member.pending,
		premium_since: member.premiumSince?.toISOString() || null,
		communication_disabled_until: member.communicationDisabledUntil?.toISOString() || null,
	} as APIGuildMember;
}

export async function getUsers(ids: string[]): Promise<void> {
	const uniqueIds = [...new Set(ids.filter(Boolean))];
	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, mapMember(member)));
		} catch (err) {
			console.error("failed to fetch guild members chunk:", err);
		}
	}

	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, { user: mapUser(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[]) {
	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.claimedBy) allIds.add(ticket.claimedBy);
		if (ticket.closedBy && ticket.closedBy !== "reporter") allIds.add(ticket.closedBy);
	}

	await getUsers(Array.from(allIds));

	return allTickets.map((ticket) => ({
		...ticket,
		authorId: ticket.authorId ? cache.get(ticket.authorId) : null,
		openedBy: ticket.openedBy ? cache.get(ticket.openedBy) : null,
		claimedBy: ticket.claimedBy ? cache.get(ticket.claimedBy) : null,
		closedBy: ticket.closedBy && ticket.closedBy !== "reporter" ? cache.get(ticket.closedBy) : null,
	}));
}