Skip to content

Commit eabfab3

Browse files
authored
Merge branch 'ZeppelinBot:master' into stock
2 parents 2f45d19 + 3372ac2 commit eabfab3

16 files changed

+325
-17
lines changed

.cursorignore

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Node template
3+
# Logs
4+
/logs
5+
*.log
6+
npm-debug.log*
7+
yarn-debug.log*
8+
yarn-error.log*
9+
.clinic
10+
.clinic-bot
11+
.clinic-api
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
25+
# nyc test coverage
26+
.nyc_output
27+
28+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29+
.grunt
30+
31+
# Bower dependency directory (https://bower.io/)
32+
bower_components
33+
34+
# node-waf configuration
35+
.lock-wscript
36+
37+
# Compiled binary addons (https://nodejs.org/api/addons.html)
38+
build/Release
39+
40+
# Dependency directories
41+
node_modules/
42+
jspm_packages/
43+
44+
# Typescript v1 declaration files
45+
typings/
46+
47+
# Optional npm cache directory
48+
.npm
49+
50+
# Optional eslint cache
51+
.eslintcache
52+
53+
# Optional REPL history
54+
.node_repl_history
55+
56+
# Output of 'npm pack'
57+
*.tgz
58+
59+
# Yarn Integrity file
60+
.yarn-integrity
61+
62+
# dotenv environment variables file
63+
*.env
64+
.env
65+
66+
# windows folder options
67+
desktop.ini
68+
69+
# PHPStorm
70+
.idea/
71+
72+
# Misc
73+
/convert.js
74+
/startscript.js
75+
.cache
76+
npm-ls.txt
77+
npm-audit.txt
78+
.vscode/launch.json
79+
80+
# Debug files
81+
*.debug.ts
82+
*.debug.js
83+
84+
.vscode/
85+
86+
config-errors.txt
87+
/config-schema.json
88+
89+
*.tsbuildinfo
90+
91+
# Legacy data folders
92+
/docker/development/data
93+
/docker/production/data

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ DEVELOPMENT_SSH_PASSWORD=password
5050
# If your user has a different UID than 1000, you might have to fill that in here to avoid permission issues
5151
#DEVELOPMENT_UID=1000
5252

53+
# If using the cftunnel docker compose profile, set your Cloudflare Tunnel token here
54+
#CF_TUNNEL_TOKEN=
55+
5356

5457
# ==========================
5558
# PRODUCTION - STANDALONE

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The project is called Zeppelin. It's a Discord bot that uses Discord.js. The bot is built on the Knub framework.
2+
3+
This repository is a monorepository that contains these projects:
4+
1. **Backend**: The shared codebase of the bot and API. Located in `backend`.
5+
2. **Dashboard**: The web dashboard that contains the bot's management interface and documentation. Located in `dashboard`.
6+
3. **Config checker**: A tool to check the configuration of the bot. Located in `config-checker`.
7+
8+
There is also a `shared` folder that contains shared code used by all projects, such as types and utilities.
9+
10+
# Backend
11+
The backend codebase is located in the `backend` directory. It contains the main bot code, API code, and shared code used by both the bot and API.
12+
Zeppelin's functionality is split into plugins, which are located in the `src/plugins` directory.
13+
Each plugin has its own directory, with a `types.ts` for config types, `docs.ts` for a `ZeppelinPluginDocs` structure, and the plugin's main file.
14+
Each plugin has an internal name, such as "common". In this example, the folder would be `src/plugins/Common` (note the capitalization). The plugin's main file would be `src/plugins/CommonPlugin.ts`.
15+
There are two types of plugins: "guild plugins" and "global plugins". Guild plugins are loaded on a per-guild basis, while global plugins are loaded once for the entire bot.
16+
Plugins can specify dependencies on other plugins and call their public methods. Likewise, plugins can specify public methods in the main file.
17+
Available plugins are specified in `src/plugins/availablePlugins.ts`.
18+
19+
Zeppelin's data layer uses TypeORM. Entities are located in `src/data/entities`, while repositories are in `src/data`. If the repository name is prefixed with "Guild", it's a guild-specific repository. If it's prefixed with "User", it's a user-specific repository. If it has no prefix, it's a global repository.
20+
21+
Environment variables are parsed in `src/env.ts`.

backend/src/data/entities/ApiPermissionAssignment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
1+
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, Relation } from "typeorm";
22
import { ApiPermissionTypes } from "../ApiPermissionAssignments.js";
33
import { ApiUserInfo } from "./ApiUserInfo.js";
44

@@ -24,5 +24,5 @@ export class ApiPermissionAssignment {
2424

2525
@ManyToOne(() => ApiUserInfo, (userInfo) => userInfo.permissionAssignments)
2626
@JoinColumn({ name: "target_id" })
27-
userInfo: ApiUserInfo;
27+
userInfo: Relation<ApiUserInfo>;
2828
}

backend/src/data/loops/expiringMutesLoop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ async function broadcastExpiredMute(guildId: string, userId: string, tries = 0):
4242
}
4343

4444
function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
45-
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
4645
if (!hasGuildEventListener(mute.guild_id, "timeoutMuteToRenew")) {
4746
// If there are no listeners registered for the server yet, try again in a bit
4847
if (tries < MAX_TRIES_PER_SERVER) {
@@ -53,6 +52,7 @@ function broadcastTimeoutMuteToRenew(mute: Mute, tries = 0) {
5352
}
5453
return;
5554
}
55+
console.log(`[EXPIRING MUTES LOOP] Broadcasting timeout mute to renew: ${mute.guild_id}/${mute.user_id}`);
5656
emitGuildEvent(mute.guild_id, "timeoutMuteToRenew", [mute]);
5757
}
5858

backend/src/data/loops/expiringTempbansLoop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ function tempbanToKey(tempban: Tempban) {
1717
}
1818

1919
function broadcastExpiredTempban(tempban: Tempban, tries = 0) {
20-
console.log(`[EXPIRING TEMPBANS LOOP] Broadcasting expired tempban: ${tempban.guild_id}/${tempban.user_id}`);
2120
if (!hasGuildEventListener(tempban.guild_id, "expiredTempban")) {
2221
// If there are no listeners registered for the server yet, try again in a bit
2322
if (tries < MAX_TRIES_PER_SERVER) {
@@ -28,6 +27,7 @@ function broadcastExpiredTempban(tempban: Tempban, tries = 0) {
2827
}
2928
return;
3029
}
30+
console.log(`[EXPIRING TEMPBANS LOOP] Broadcasting expired tempban: ${tempban.guild_id}/${tempban.user_id}`);
3131
emitGuildEvent(tempban.guild_id, "expiredTempban", [tempban]);
3232
}
3333

backend/src/data/loops/upcomingScheduledPostsLoop.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,21 @@ export function registerUpcomingScheduledPost(post: ScheduledPost) {
5353
return;
5454
}
5555

56-
console.log("[SCHEDULED POSTS LOOP] Registering new upcoming scheduled post");
5756
const remaining = Math.max(0, moment.utc(post.post_at).diff(moment.utc()));
5857
if (remaining > LOOP_INTERVAL) {
5958
return;
6059
}
6160

61+
console.log("[SCHEDULED POSTS LOOP] Registering new upcoming scheduled post");
6262
timeouts.set(
6363
post.id,
6464
setTimeout(() => broadcastScheduledPost(post), remaining),
6565
);
6666
}
6767

6868
export function clearUpcomingScheduledPost(post: ScheduledPost) {
69-
console.log("[SCHEDULED POSTS LOOP] Clearing upcoming scheduled post");
7069
if (timeouts.has(post.id)) {
70+
console.log("[SCHEDULED POSTS LOOP] Clearing upcoming scheduled post");
7171
clearTimeout(timeouts.get(post.id)!);
7272
}
7373
}

backend/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,12 @@ connect().then(async () => {
276276
}
277277
});
278278

279+
let ignorePluginLoadErrors = true;
279280
client.on("error", (err) => {
280281
if (err instanceof PluginLoadError) {
281-
errorHandler(err);
282+
if (!ignorePluginLoadErrors) {
283+
errorHandler(err);
284+
}
282285
return;
283286
}
284287
errorHandler(new DiscordJSError(err.message, (err as any).code, 0));
@@ -411,6 +414,8 @@ connect().then(async () => {
411414
enableProfiling();
412415
}
413416

417+
ignorePluginLoadErrors = false;
418+
414419
initFishFish();
415420

416421
runExpiringMutesLoop();

backend/src/plugins/Automod/AutomodPlugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChang
3333
import { clearOldRecentActions } from "./functions/clearOldRecentActions.js";
3434
import { clearOldRecentSpam } from "./functions/clearOldRecentSpam.js";
3535
import { AutomodPluginType, zAutomodConfig } from "./types.js";
36+
import { DebugAutomodCmd } from "./commands/DebugAutomodCmd.js";
3637

3738
export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
3839
name: "automod",
@@ -67,7 +68,7 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
6768
// Messages use message events from SavedMessages, see onLoad below
6869
],
6970

70-
messageCommands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd],
71+
messageCommands: [AntiraidClearCmd, SetAntiraidCmd, ViewAntiraidCmd, DebugAutomodCmd],
7172

7273
async beforeLoad(pluginData) {
7374
const { state, guild } = pluginData;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { guildPluginMessageCommand } from "knub";
2+
import moment from "moment-timezone";
3+
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
4+
import { AutomodContext, AutomodPluginType } from "../types.js";
5+
import { runAutomod } from "../functions/runAutomod.js";
6+
import { createChunkedMessage } from "../../../utils.js";
7+
import { getOrFetchGuildMember } from "../../../utils/getOrFetchGuildMember.js";
8+
import { getOrFetchUser } from "../../../utils/getOrFetchUser.js";
9+
10+
export const DebugAutomodCmd = guildPluginMessageCommand<AutomodPluginType>()({
11+
trigger: "debug_automod",
12+
permission: "can_debug_automod",
13+
14+
signature: {
15+
messageId: ct.string(),
16+
},
17+
18+
async run({ pluginData, message, args }) {
19+
const targetMessage = await pluginData.state.savedMessages.find(args.messageId);
20+
if (!targetMessage || targetMessage.guild_id !== pluginData.guild.id) {
21+
pluginData.state.common.sendErrorMessage(message, "Message not found");
22+
return;
23+
}
24+
25+
const member = await getOrFetchGuildMember(pluginData.guild, targetMessage.user_id);
26+
const user = await getOrFetchUser(pluginData.client, targetMessage.user_id);
27+
const context: AutomodContext = {
28+
timestamp: moment.utc(targetMessage.posted_at).valueOf(),
29+
message: targetMessage,
30+
user,
31+
member,
32+
};
33+
34+
const result = await runAutomod(pluginData, context, true);
35+
36+
let resultText = `**${result.triggered ? "✔️ Triggered" : "❌ Not triggered"}**\n\nRules checked:\n\n`;
37+
for (const ruleResult of result.rulesChecked) {
38+
resultText += `**${ruleResult.ruleName}**\n`;
39+
if (ruleResult.outcome.success) {
40+
resultText += `\\- Matched trigger: ${ruleResult.outcome.matchedTrigger.name} (trigger #${ruleResult.outcome.matchedTrigger.num})\n`;
41+
} else {
42+
resultText += `\\- No match (${ruleResult.outcome.reason})\n`;
43+
}
44+
}
45+
46+
createChunkedMessage(message.channel, resultText.trim());
47+
},
48+
});

0 commit comments

Comments
 (0)