feat: use webhooks for logs when possible
This commit is contained in:
parent
1081d1b361
commit
55a39e0758
12 changed files with 318 additions and 29 deletions
39
backend/src/plugins/InternalPoster/InternalPosterPlugin.ts
Normal file
39
backend/src/plugins/InternalPoster/InternalPosterPlugin.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { PluginOptions, typedGuildCommand } from "knub";
|
||||
import { GuildPingableRoles } from "../../data/GuildPingableRoles";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { ConfigSchema, InternalPosterPluginType } from "./types";
|
||||
import {
|
||||
getPhishermanDomainInfo,
|
||||
hasPhishermanMasterAPIKey,
|
||||
phishermanApiKeyIsValid,
|
||||
reportTrackedDomainsToPhisherman,
|
||||
} from "../../data/Phisherman";
|
||||
import { mapToPublicFn } from "../../pluginUtils";
|
||||
import { Webhooks } from "../../data/Webhooks";
|
||||
import { Queue } from "../../Queue";
|
||||
import { sendMessage } from "./functions/sendMessage";
|
||||
|
||||
const defaultOptions: PluginOptions<InternalPosterPluginType> = {
|
||||
config: {},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
export const InternalPosterPlugin = zeppelinGuildPlugin<InternalPosterPluginType>()({
|
||||
name: "internal_poster",
|
||||
showInDocs: false,
|
||||
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
|
||||
// prettier-ignore
|
||||
public: {
|
||||
sendMessage: mapToPublicFn(sendMessage),
|
||||
},
|
||||
|
||||
async beforeLoad(pluginData) {
|
||||
pluginData.state.webhooks = new Webhooks();
|
||||
pluginData.state.queue = new Queue();
|
||||
pluginData.state.missingPermissions = false;
|
||||
pluginData.state.webhookClientCache = new Map();
|
||||
},
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { InternalPosterPluginType } from "../types";
|
||||
import { NewsChannel, Permissions, TextChannel } from "discord.js";
|
||||
import { isDiscordAPIError } from "../../../utils";
|
||||
|
||||
type WebhookInfo = [id: string, token: string];
|
||||
|
||||
export async function getOrCreateWebhookForChannel(
|
||||
pluginData: GuildPluginData<InternalPosterPluginType>,
|
||||
channel: TextChannel | NewsChannel,
|
||||
): Promise<WebhookInfo | null> {
|
||||
// tslint:disable-next-line:no-console FIXME: Here for debugging purposes
|
||||
console.log(`getOrCreateWebhookForChannel(${channel.id})`);
|
||||
|
||||
// Database cache
|
||||
const fromDb = await pluginData.state.webhooks.findByChannelId(channel.id);
|
||||
if (fromDb) {
|
||||
return [fromDb.id, fromDb.token];
|
||||
}
|
||||
|
||||
if (pluginData.state.missingPermissions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new webhook
|
||||
const member = pluginData.client.user && pluginData.guild.members.cache.get(pluginData.client.user.id);
|
||||
if (!member || member.permissions.has(Permissions.FLAGS.MANAGE_WEBHOOKS)) {
|
||||
try {
|
||||
const webhook = await channel.createWebhook(`Zephook ${channel.id}`);
|
||||
await pluginData.state.webhooks.create({
|
||||
id: webhook.id,
|
||||
guild_id: pluginData.guild.id,
|
||||
channel_id: channel.id,
|
||||
token: webhook.token!,
|
||||
});
|
||||
return [webhook.id, webhook.token!];
|
||||
} catch (err) {
|
||||
if (isDiscordAPIError(err) && err.code === 50013) {
|
||||
pluginData.state.missingPermissions = true;
|
||||
console.warn(`Error ${err.code} when trying to create webhook for ${pluginData.guild.id}`);
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
71
backend/src/plugins/InternalPoster/functions/sendMessage.ts
Normal file
71
backend/src/plugins/InternalPoster/functions/sendMessage.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Message, MessageOptions, NewsChannel, TextChannel, WebhookClient } from "discord.js";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { InternalPosterPluginType } from "../types";
|
||||
import { getOrCreateWebhookForChannel } from "./getOrCreateWebhookForChannel";
|
||||
import { APIMessage } from "discord-api-types";
|
||||
import { isDiscordAPIError } from "../../../utils";
|
||||
|
||||
export type InternalPosterMessageResult = {
|
||||
id: string;
|
||||
channelId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a message using a webhook or direct API requests, preferring webhooks when possible.
|
||||
*/
|
||||
export async function sendMessage(
|
||||
pluginData: GuildPluginData<InternalPosterPluginType>,
|
||||
channel: TextChannel | NewsChannel,
|
||||
content: MessageOptions,
|
||||
): Promise<InternalPosterMessageResult> {
|
||||
return pluginData.state.queue.add(async () => {
|
||||
if (!pluginData.state.webhookClientCache.has(channel.id)) {
|
||||
const webhookInfo = await getOrCreateWebhookForChannel(pluginData, channel);
|
||||
if (webhookInfo) {
|
||||
const client = new WebhookClient({
|
||||
id: webhookInfo[0],
|
||||
token: webhookInfo[1],
|
||||
});
|
||||
pluginData.state.webhookClientCache.set(channel.id, client);
|
||||
} else {
|
||||
pluginData.state.webhookClientCache.set(channel.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
const webhookClient = pluginData.state.webhookClientCache.get(channel.id);
|
||||
if (webhookClient) {
|
||||
return webhookClient
|
||||
.send({
|
||||
...content,
|
||||
...(pluginData.client.user && {
|
||||
username: pluginData.client.user.username,
|
||||
avatarURL: pluginData.client.user.avatarURL() || pluginData.client.user.defaultAvatarURL,
|
||||
}),
|
||||
})
|
||||
.then((apiMessage) => ({
|
||||
id: apiMessage.id,
|
||||
channelId: apiMessage.channel_id,
|
||||
}))
|
||||
.catch(async (err) => {
|
||||
// Unknown Webhook
|
||||
if (isDiscordAPIError(err) && err.code === 10015) {
|
||||
await pluginData.state.webhooks.delete(webhookClient.id);
|
||||
pluginData.state.webhookClientCache.delete(channel.id);
|
||||
|
||||
// Fallback to regular message for this log message
|
||||
return channel.send(content).then((message) => ({
|
||||
id: message.id,
|
||||
channelId: message.channelId,
|
||||
}));
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return channel.send(content).then((message) => ({
|
||||
id: message.id,
|
||||
channelId: message.channelId,
|
||||
}));
|
||||
});
|
||||
}
|
22
backend/src/plugins/InternalPoster/types.ts
Normal file
22
backend/src/plugins/InternalPoster/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as t from "io-ts";
|
||||
import { BasePluginType } from "knub";
|
||||
import { Webhooks } from "../../data/Webhooks";
|
||||
import { Queue } from "../../Queue";
|
||||
import { WebhookClient } from "discord.js";
|
||||
|
||||
export const ConfigSchema = t.type({});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
// <channelId, webhookUrl>
|
||||
type ChannelWebhookMap = Map<string, string>;
|
||||
|
||||
export interface InternalPosterPluginType extends BasePluginType {
|
||||
config: TConfigSchema;
|
||||
|
||||
state: {
|
||||
queue: Queue;
|
||||
webhooks: Webhooks;
|
||||
missingPermissions: boolean;
|
||||
webhookClientCache: Map<string, WebhookClient | null>;
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue