diff --git a/src/plugins/ZeppelinPlugin.ts b/src/plugins/ZeppelinPlugin.ts index 325a8f17..d91bcb8c 100644 --- a/src/plugins/ZeppelinPlugin.ts +++ b/src/plugins/ZeppelinPlugin.ts @@ -4,7 +4,7 @@ import * as t from "io-ts"; import { pipe } from "fp-ts/lib/pipeable"; import { fold } from "fp-ts/lib/Either"; import { PathReporter } from "io-ts/lib/PathReporter"; -import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils"; +import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, resolveUserId, UnknownUser } from "../utils"; import { Member, User } from "eris"; import { performance } from "perf_hooks"; import { validateStrict } from "../validatorUtils"; @@ -126,14 +126,24 @@ export class ZeppelinPlugin extends Plug * Resolves a member from the passed string. The passed string can be a user id, a user mention, a full username (with discrim), etc. * If the member is not found in the cache, it's fetched from the API. */ - async getMember(memberResolvable: string): Promise { + async getMember(memberResolvable: string, forceFresh = false): Promise { const start = performance.now(); - const member = await resolveMember(this.bot, this.guild, memberResolvable); + + let member; + if (forceFresh) { + const userId = await resolveUserId(this.bot, memberResolvable); + member = userId && (await this.bot.getRESTGuildMember(this.guild.id, userId)); + if (member) member.id = member.user.id; + } else { + member = await resolveMember(this.bot, this.guild, memberResolvable); + } + const time = performance.now() - start; if (time >= SLOW_RESOLVE_THRESHOLD) { const rounded = Math.round(time); logger.warn(`Slow member resolve (${rounded}ms): ${memberResolvable} in ${this.guild.name} (${this.guild.id})`); } + return member; } } diff --git a/src/utils.ts b/src/utils.ts index c41fa665..3ef6ee21 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,6 +31,12 @@ const delayStringMultipliers = { s: 1000, }; +export const MS = 1; +export const SECONDS = 1000 * MS; +export const MINUTES = 60 * SECONDS; +export const HOURS = 60 * MINUTES; +export const DAYS = 24 * HOURS; + export function tNullable(type: t.Mixed) { return t.union([type, t.undefined, t.null]); } @@ -571,91 +577,86 @@ export class UnknownUser { const unknownUsers = new Set(); const unknownMembers = new Set(); +export function resolveUserId(bot: Client, value: string) { + // A user mention? + const mentionMatch = value.match(/^<@!?(\d+)>$/); + if (mentionMatch) { + return mentionMatch[1]; + } + + // A non-mention, full username? + const usernameMatch = value.match(/^@?([^#]+)#(\d{4})$/); + if (usernameMatch) { + const user = bot.users.find(u => u.username === usernameMatch[1] && u.discriminator === usernameMatch[2]); + if (user) return user.id; + } + + // Just a user ID? + const idMatch = value.match(/^\d+$/); + if (idMatch) { + return value; + } + + return null; +} + export async function resolveUser(bot: Client, value: string): Promise { if (value == null || typeof value !== "string") { return new UnknownUser(); } - let userId; - - // A user mention? - const mentionMatch = value.match(/^<@!?(\d+)>$/); - if (mentionMatch) { - userId = mentionMatch[1]; + // If we have the user cached, return that directly + const userId = resolveUserId(bot, value); + if (bot.users.has(userId)) { + return bot.users.get(userId); } - // A non-mention, full username? - if (!userId) { - const usernameMatch = value.match(/^@?([^#]+)#(\d{4})$/); - if (usernameMatch) { - const user = bot.users.find(u => u.username === usernameMatch[1] && u.discriminator === usernameMatch[2]); - if (user) userId = user.id; - } + // We don't want to spam the API by trying to fetch unknown users again and again, + // so we cache the fact that they're "unknown" for a while + if (unknownUsers.has(userId)) { + return new UnknownUser({ id: userId }); } - // Just a user ID? - if (!userId) { - const idMatch = value.match(/^\d+$/); - if (!idMatch) { - return null; - } - - userId = value; + const freshUser = await bot.getRESTUser(userId); + if (freshUser) { + bot.users.add(freshUser, bot); + return freshUser; } - const cachedUser = bot.users.find(u => u.id === userId); - if (cachedUser) return cachedUser; - - // We only fetch the user from the API if we haven't tried it before: - // - If the user was found, the bot has them in its cache - // - If the user was not found, they'll be in unknownUsers - if (!unknownUsers.has(userId)) { - try { - const freshUser = await bot.getRESTUser(userId); - bot.users.add(freshUser, bot); - return freshUser; - } catch (e) {} // tslint:disable-line - - unknownUsers.add(userId); - } + unknownUsers.add(userId); + setTimeout(() => unknownUsers.delete(userId), 15 * MINUTES); return new UnknownUser({ id: userId }); } export async function resolveMember(bot: Client, guild: Guild, value: string): Promise { - // Start by resolving the user - const user = await resolveUser(bot, value); - if (!user || user instanceof UnknownUser) return null; + const userId = resolveUserId(bot, value); + if (!userId) return null; - // See if we have the member cached... - let member = guild.members.get(user.id); - - // We only fetch the member from the API if we haven't tried it before: - // - If the member was found, the bot has them in the guild's member cache - // - If the member was not found, they'll be in unknownMembers - const unknownKey = `${guild.id}-${user.id}`; - if (!unknownMembers.has(unknownKey)) { - // If not, fetch it from the API - if (!member) { - try { - logger.debug(`Fetching unknown member (${user.id} in ${guild.name} (${guild.id})) from the API`); - - member = await bot.getRESTGuildMember(guild.id, user.id); - member.id = user.id; - member.guild = guild; - } catch (e) {} // tslint:disable-line - } - - if (!member) unknownMembers.add(unknownKey); + // If we have the member cached, return that directly + if (guild.members.has(userId)) { + return guild.members.get(userId); } - return member; + // We don't want to spam the API by trying to fetch unknown members again and again, + // so we cache the fact that they're "unknown" for a while + const unknownKey = `${guild.id}-${userId}`; + if (unknownMembers.has(unknownKey)) { + return null; + } + + logger.debug(`Fetching unknown member (${userId} in ${guild.name} (${guild.id})) from the API`); + + const freshMember = await bot.getRESTGuildMember(guild.id, userId).catch(noop); + if (freshMember) { + freshMember.id = userId; + return freshMember; + } + + unknownMembers.add(unknownKey); + setTimeout(() => unknownMembers.delete(unknownKey), 15 * MINUTES); + + return null; } -export const MS = 1; -export const SECONDS = 1000 * MS; -export const MINUTES = 60 * SECONDS; -export const HOURS = 60 * MINUTES; -export const DAYS = 24 * HOURS; - export type StrictMessageContent = { content?: string; tts?: boolean; disableEveryone?: boolean; embed?: EmbedOptions };