Skip to content

Commit

Permalink
feature: bot framework
Browse files Browse the repository at this point in the history
  • Loading branch information
apacheli committed Jun 29, 2024
1 parent 3680be5 commit 998a8cb
Show file tree
Hide file tree
Showing 21 changed files with 312 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .vsocde/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"deno.enable": false
}
5 changes: 5 additions & 0 deletions core/bot/bot_command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const botCommand = (id, handler, options) => ({
id,
handler,
options,
});
15 changes: 15 additions & 0 deletions core/bot/commands/args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { botCommand } from "../bot_command.js";

export default botCommand("args", (_bot, _, ctx) => {
return {
data: {
content: `\`\`\`json\n${JSON.stringify(ctx.parsed, null, 2)}\n\`\`\``,
},
};
}, {
description: "Display command arguments. Use `--` to specify a keyword argument.",
category: "dev",
parseMode: 1,
usage: "[...args]",
dev: false,
});
12 changes: 12 additions & 0 deletions core/bot/commands/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { botCommand } from "../bot_command.js";

export default botCommand("args", (_bot, _, ctx) => {
const date = ctx.parsed === null ? Date.now() : new Date(ctx.parsed).getTime();
return { data: `{"content":"<t:${Math.floor(date / 1000)}>"}` };
}, {
description: "Display the date.",
category: "info",
parseMode: 2,
usage: "[...date]",
dev: false,
});
11 changes: 11 additions & 0 deletions core/bot/commands/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { botCommand } from "../bot_command.js";

export default botCommand("error", () => {
throw new Error("An error occurred.");
}, {
description: "Throw an error. Developer only.",
category: "dev",
parseMode: 0,
usage: null,
dev: true,
});
15 changes: 15 additions & 0 deletions core/bot/commands/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { botCommand } from "../bot_command.js";

export default botCommand("events", (bot) => {
let str = "";
for (const key in bot.receivedEvents) {
str += `${key}: ${bot.receivedEvents[key]}\n`;
}
return { data: { content: `\`\`\`yaml\n${str}\n\`\`\`` } };
}, {
description: "Display the events received.",
category: "dev",
parseMode: 0,
usage: null,
dev: false,
});
63 changes: 63 additions & 0 deletions core/bot/commands/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { botCommand } from "../bot_command.js";

const _cached = new Map();
let _commands;
const _notFound = { data: JSON.stringify({ content: "command not found" }) };

export default botCommand("help", (bot, _, ctx) => {
if (ctx.parsed === null) {
if (_commands !== undefined) {
return _commands;
}
const categories = {};
for (const command of bot.commands.values()) {
if (categories[command.options.category] === undefined) {
categories[command.options.category] = "";
}
categories[command.options.category] += `, \`${command.id}\``;
}
const fields = [];
for (const name in categories) {
fields.push({ name, value: categories[name].slice(2) });
}
const embed = {
title: "Commands",
color: bot.config.themeColor,
fields,
footer: {
text: `Type ${bot.config.prefix}help [command] for more information.`,
},
};
_commands = { data: JSON.stringify({ embeds: [embed] }) };
return _commands;
}
const cached = _cached.get(ctx.parsed);
if (cached !== undefined) {
return cached;
}
const command = bot.commands.get(ctx.parsed);
if (command === undefined) {
return _notFound;
}
let u = `${bot.config.prefix}${command.id}`;
if (command.options.usage !== null) {
u += ` ${command.options.usage}`;
}
const embed = {
title: command.id,
color: bot.config.themeColor,
description: `\`\`\`\n${u}\n\`\`\`\n${command.options.description}`,
footer: {
text: command.options.category,
},
};
const response = { data: JSON.stringify({ embeds: [embed] }) };
_cached.set(command.id, response);
return response;
}, {
description: "Display a list of commands or command details.",
category: "info",
parseMode: 2,
usage: "[...command]",
dev: false,
});
36 changes: 36 additions & 0 deletions core/bot/commands/info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pkg from "../../../package.json" with { type: "json" };
import { field } from "../../util/embed.js";
import { botCommand } from "../bot_command.js";

export default botCommand("info", (bot) => {
let latency = 0;
for (const shard of bot.gateway.shards.values()) {
latency += shard.latency;
}
let events = 0;
for (const event in bot.receivedEvents) {
events += bot.receivedEvents[event];
}
const embed = {
title: "Information",
color: bot.config.themeColor,
fields: [
field("Bun", `\`${process.versions.bun}\``, true),
field("Version", `\`${bot.config.version ?? "latest"}\``, true),
field("whirlybird", `\`${pkg.version}\``, true),
field("Events", events, true),
field("Guilds", bot.cache.guilds.size, true),
field("Users", bot.cache.users.size, true),
field("Latency", `${latency / bot.gateway.shards.size} ms`, true),
field("Memory", `${Math.floor(process.memoryUsage().heapUsed / 1_048_576 * 100) / 100} MiB`, true),
field("Uptime", `${Math.floor(process.uptime() / 60 * 100) / 100} m`, true),
],
};
return { data: { embeds: [embed] } };
}, {
description: "Display information about the bot.",
category: "info",
parseMode: 0,
usage: null,
dev: false,
});
22 changes: 22 additions & 0 deletions core/bot/commands/js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as whirlybird from "../../lib.js";
import { botCommand } from "../bot_command.js";

const _inspect = { depth: 0 };

export default botCommand("js", async (bot, message, ctx) => {
let _result;
try {
_result = await eval(ctx.parsed);
} catch (error) {
_result = error;
}
return {
files: [new File([Bun.inspect(_result, _inspect)], `${Date.now()}.js`)],
};
}, {
description: "Run JavaScript code. Developer only.",
category: "dev",
parseMode: 2,
usage: "[...code]",
dev: true,
});
11 changes: 11 additions & 0 deletions core/bot/commands/ping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { botCommand } from "../bot_command.js";

const _ping = { data: '{"content":":ping_pong: Pong!"}' };

export default botCommand("ping", () => _ping, {
description: "Ping the bot.",
category: "dev",
parseMode: 0,
usage: null,
dev: false,
});
47 changes: 47 additions & 0 deletions core/bot/create_bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CacheClient } from "../cache/cache_client.js";
import { GatewayClient } from "../gateway/gateway_client.js";
import { RestClient } from "../rest/rest_client.js";
import { EventDispatcher } from "../util/event_dispatcher.js";

import _args from "./commands/args.js";
import _date from "./commands/date.js";
import _error from "./commands/error.js";
import _events from "./commands/events.js";
import _help from "./commands/help.js";
import _info from "./commands/info.js";
import _js from "./commands/js.js";
import _ping from "./commands/ping.js";

import _messageCreate from "./events/message_create.js";
import _messageReactionAdd from "./events/message_reaction_add.js";

export const createBot = (token, options) => {
const dispatcher = new EventDispatcher();
dispatcher.listen("MESSAGE_CREATE", _messageCreate);
dispatcher.listen("MESSAGE_REACTION_ADD", _messageReactionAdd);
const bot = {
cache: new CacheClient(options.cache),
commands: new Map()
.set("args", _args)
.set("date", _date)
.set("error", _error)
.set("events", _events)
.set("help", _help)
.set("info", _info)
.set("js", _js)
.set("ping", _ping),
config: options.config,
dispatcher,
receivedEvents: {},
gateway: new GatewayClient(token, {
handleEvent: (event, data, shard) => {
bot.receivedEvents[event] = (bot.receivedEvents[event] ?? 0) + 1;
bot.cache.handleEvent(event, data);
bot.dispatcher.dispatch(event, bot, data, shard);
},
...options.gateway,
}),
rest: new RestClient(token, options.rest),
};
return bot;
};
49 changes: 49 additions & 0 deletions core/bot/events/message_create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { parse } from "../../util/args.js";
import { log } from "../../util/log.js";

export default async (bot, message) => {
if (
message.author.bot ||
message.content.slice(0, bot.config.prefix.length) !== bot.config.prefix
) {
return;
}
const content = message.content.slice(bot.config.prefix.length);
const index = content.indexOf(" ");
const name = index === -1 ? content : content.slice(0, index);
const command = bot.commands.get(name.toLowerCase());
if (command === undefined) {
log("ERR", `${name}: command not found`);
return;
}
if (command.options.dev && message.author.id !== bot.config.developerId) {
log("ERR", `${name}: executor is not developer`);
return;
}
const ctx = {
command,
parsed: null,
};
if (index !== -1) {
switch (command.options.parseMode) {
case 1: {
ctx.parsed = parse(content.slice(index + 1));
break;
}

case 2: {
ctx.parsed = content.slice(index + 1);
break;
}
}
}
try {
const response = await command.handler(bot, message, ctx);
if (response !== undefined) {
bot.rest.createMessage(message.channel_id, response);
}
} catch (error) {
log("ERR", `${command.id}: execution error:`);
console.error(error);
}
};
9 changes: 9 additions & 0 deletions core/bot/events/message_reaction_add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default (bot, event) => {
if (
event.user_id === bot.config.developerId &&
event.message_author_id === bot.cache.application.id &&
event.emoji.name === "\u274c"
) {
bot.rest.deleteMessage(event.channel_id, event.message_id);
}
};
2 changes: 2 additions & 0 deletions core/bot/lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./bot_command.js";
export * from "./create_bot.js";
1 change: 1 addition & 0 deletions core/bot/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# `whirlybird/bot`
7 changes: 3 additions & 4 deletions core/cache/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ export const updateMessage = (message, data) => {
message.mentionEveryone = data.mention_everyone;
}
if (data.mentions !== undefined) {
message.mentions = data.mentions.map(mapId);
message.mentions = data.mentions.map(_mapId);
}
if (data.mention_roles !== undefined) {
message.mentionRoles = data.mention_roles.map(BigInt);
}
if (data.mention_channels !== undefined) {
message.mentionChannels = data.mention_channels.map(mapId);
message.mentionChannels = data.mention_channels.map(_mapId);
}
if (data.attachments !== undefined) {
message.attachments = data.attachments;
Expand Down Expand Up @@ -91,7 +91,7 @@ export const updateMessage = (message, data) => {
return message;
};

const mapId = (item) => BigInt(item.id);
const _mapId = (item) => BigInt(item.id);

/** https://discord.com/developers/docs/resources/channel#message-object-message-types */
export const MessageType = {
Expand Down Expand Up @@ -125,7 +125,6 @@ export const MessageType = {
STAGE_TOPIC: 31,
GUILD_APPLICATION_PREMIUM_SUBSCRIPTION: 32,
GUILD_INCIDENT_ALERT_NODE_ENABLED: 37,
GUILD_INCIDENT_ALERT_NODE_ENABLED: 38,
GUILD_INCIDENT_REPORT_RAID: 38,
GUILD_INCIDENT_REPORT_FALSE_ALARM: 39,
PURCHASE_NOTIFICATION: 44,
Expand Down
1 change: 1 addition & 0 deletions core/lib.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./bot/lib.js";
export * from "./cache/lib.js";
export * from "./gateway/lib.js";
export * from "./interactions/lib.js";
Expand Down
5 changes: 5 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"fmt": {
"lineWidth": 320
}
}
19 changes: 0 additions & 19 deletions dprint.json

This file was deleted.

Loading

0 comments on commit 998a8cb

Please sign in to comment.