Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make bot suitable for multiple servers #67

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
85c8760
Move guild settings to database
TrojanerHD Jan 14, 2023
fd70c16
Make no permitted roles a status rather than a warning since it's not…
TrojanerHD Jan 15, 2023
4995264
Add roles command to dynamically change roles for the roles channel
TrojanerHD Jan 15, 2023
a6ec446
Add permit command to dynamically change permitted roles
TrojanerHD Jan 15, 2023
e234055
Use role id instead of name in permit command
TrojanerHD Jan 15, 2023
2d747db
Add stream-channel command to dynamically manage streams shown in the…
TrojanerHD Jan 15, 2023
71c2ce7
Update README for multi-server deployment
TrojanerHD Jan 15, 2023
5a9aa1c
Fix live channel embed being the same for all guilds
TrojanerHD Jan 15, 2023
cbbc884
Fix link in README to give the bot admin permissions on the server
TrojanerHD Mar 5, 2023
b9720d6
Fix stream channel remove removing everyone else
TrojanerHD Mar 5, 2023
7715410
Make stream channel remove streamer is not added message ephemeral
TrojanerHD Mar 5, 2023
9c783f2
Delete live message if stream channel remove makes streamer array empty
TrojanerHD Mar 5, 2023
8039320
Add more JSDoc to TwitchHelper
TrojanerHD Mar 5, 2023
f674a3c
Add more JSDoc to FeatureChecker
TrojanerHD Mar 5, 2023
b07350b
Execute RoleChannelManager and LiveChannel when joining a new guild
TrojanerHD Mar 5, 2023
30d3291
Remove unused settings import in live channel
TrojanerHD Mar 12, 2023
0562f9b
Bind this on guild create so this calls don't cause a crash
TrojanerHD Mar 12, 2023
de52e50
Handle deletion / renaming of roles channel
TrojanerHD Mar 12, 2023
ddf631b
Extract getRoleChannelManager into common to use it in RolesCommand
TrojanerHD Mar 12, 2023
dbafe11
Move channel deletion / renaming functions into RoleChannelManager
TrojanerHD Mar 12, 2023
98d344e
Update types
TrojanerHD Mar 12, 2023
8cd3e63
Add awaits and catches for saveSettings function
TrojanerHD Mar 12, 2023
b9717e1
Make onMessage not async anymore
TrojanerHD Mar 12, 2023
3d57787
Correct syntax of stream-channel command in README
TrojanerHD Mar 12, 2023
e1f9748
Use role id for permit command and add list subcommand
TrojanerHD Mar 18, 2023
605c754
Fix typo in RolesCommand
TrojanerHD Mar 18, 2023
ce7ec95
Add more JSDoc to RoleChannelManager
TrojanerHD Mar 18, 2023
63a7386
Simplify sql functions for insert and update
TrojanerHD Mar 18, 2023
455ec2f
Rename insertOrUpdate to upsert
TrojanerHD Mar 18, 2023
9e824dd
Add refresh token for each server
TrojanerHD Mar 18, 2023
73d79e1
Add notes in README that optional init commands can be done later
TrojanerHD May 20, 2023
e792bc7
Merge branch 'main' into database
TrojanerHD Jul 11, 2023
c4ff6ef
Fix all problems that occured due to the merge
TrojanerHD Jul 11, 2023
6159b6d
Only update commands when /permit changed something
TrojanerHD Jul 11, 2023
be2da22
Update JSDoc and fix newline issues
TrojanerHD Jul 11, 2023
467feae
Update JSDoc in MessageHandler
TrojanerHD Jul 11, 2023
122ee6c
Extract Listener interface in Authentication.ts
TrojanerHD Sep 8, 2023
bdbb762
Rename access token callback, extract permission function, and update…
TrojanerHD Sep 8, 2023
a5f07af
Change permit command to not redeploy commands
TrojanerHD Sep 8, 2023
c9384fa
Only remove authentication listener if it exists
TrojanerHD Sep 8, 2023
087688c
Change order of functions in Authentication.ts
TrojanerHD Sep 8, 2023
7a8142a
Extract token related functions into a new TokenHelper class
TrojanerHD Sep 8, 2023
ad8c527
Extract server related functions into new class AuthenticationServer …
TrojanerHD Sep 8, 2023
9d95b3d
Add JSDoc to authentication callback
TrojanerHD Sep 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/node_modules/
yarn-error.log
/build/
settings.json
settings.json
settings.db
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Trojaner Bot
This is a [Discord](https://discord.com) bot currently only running on my [Discord server](https://discord.gg/NdsmmwV). There is no official way of adding this bot to your own Discord server.
This is a [Discord](https://discord.com) bot currently only running on my [Discord server](https://discord.gg/NdsmmwV). To add this bot on your server, use the following link:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is a [Discord](https://discord.com) bot currently only running on my [Discord server](https://discord.gg/NdsmmwV). To add this bot on your server, use the following link:
This is a [Discord](https://discord.com) bot currently only running on my [Discord server](https://discord.gg/NdsmmwV). To add this bot on your server, follow these steps:


## Adding the bot to your server
1. Click the following link: https://discord.com/api/oauth2/authorize?client_id=632637013475983360&permissions=8&scope=bot%20applications.commands
- Note that the bot is added with admin privileges. You may alter the permissions value but keep in mind that some features might not work or lead to unexpected behavior
2. (Optional, can be done later) Set up the following text-channels
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. (Optional, can be done later) Set up the following text-channels
2. (Optional, can be done later) Set up the following text-channels:

- `#live` for a nice embed showing people selected by you who are currently live on Twitch
- `#roles` for a customizable role management
3. (Optional, can be done later) Set up the bot using the following admin commands:
- `/permit <add|remove> <role>` adds/removes `<role>` as permitted. Every user with that role can execute privileged commands.
- `/stream-channel <option|list> …` adds Twitch streamers to show in a `#live` channel. If a channel called `#live` does not exist, this does not work
- `/roles <add|remove> <role> …` adds/removes roles for the role manager (channel `#roles` is required)

## Deployment and contribution
### Requirements
Comment on lines 15 to 16
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docker deployment explanation

Expand All @@ -24,16 +35,13 @@ Where you replace…
- `<TWITCH_TOKEN> (optional)` with your client secret generated after you create an application in the [Twitch Developer Console](https://dev.twitch.tv/console/apps) which is only required if you want to use the Twitch features of this bot
- `<OAUTH_TOKEN>` with the OAuth token you get from your application's OAuth page (`https://discord.com/developers/applications/{YOUR_APP_ID}/oauth2/general` → Client Secret)

Execute the bot as described in [Execution](https://github.com/TrojanerHD/TrojanerBot/#Execution). It will generate a `settings.json` file in the bot directory and will already work. You can, however, modify the content of `settings.json` (you need to restart the bot if you added a Twitch ID or roles):
Execute the bot as described in [Execution](https://github.com/TrojanerHD/TrojanerBot/#Execution). It will generate a `settings.json` file in the bot directory and will already work. You can, however, modify the content of `settings.json` (you need to restart the bot if you added a Twitch ID):
#### Settings
In this step I will go over all properties of the `settings.json` file:

Key | Type | Description | Default
--- | --- | --- | ---
`twitch-id` | string | The ID of your Twitch application found in the [Twitch Developer Console](https://dev.twitch.tv/console/apps). If you do not want to use the Twitch features of this bot, set the value to `""` | `""`
`permission-roles` | string[] | Added roles will be able to execute commands that require permissions (e. g. /bye) | `[]`
`roles` | {name: string; emoji: string; description?: string}[] | Added roles will be displayed in a `#roles` channel in Discord if the Discord has this channel. The emoji id will be used as reaction emoji so when a user reacts with the certain emoji they will get the role respectively. If you do not want to use this feature, set the value to `[]` | `[]`
`streamers` | string[] | The bot will show whenever the here specified channels are live in a `#live` channel in Discord if the Discord has this channel. If you do not want to use this feature, set the value to `[]` | `[]`
`streamer-subscriptions` | {streamer: string; subscribers: string[]} | The bot will store all subscriptions to Twitch streamers when somebody uses `/streamer` to subscribe to a streamer. Usually you want to leave it as it is | `[]`
`logging` | "errors" \| "warnings" \| "verbose" | The log-level. "warnings" will inform if, for example, twitch id was set but a token was not provided | `"warnings"`
`express-port` | number \| undefined | The port the express app will listen on | `undefined`
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"discord.js": "^14.11.0",
"dotenv": "^16.0.0",
"express": "^4.18.1"
"express": "^4.18.1",
"sqlite3": "^5.1.2"
}
}
110 changes: 96 additions & 14 deletions src/DiscordClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
ThreadChannel,
ThreadMember,
TextBasedChannel,
Guild,
NonThreadGuildBasedChannel,
DMChannel,
GatewayIntentBits,
BaseMessageOptions,
} from 'discord.js';
Expand All @@ -16,13 +19,15 @@ import RoleChannelManager from './roles/RoleChannelManager';
import Settings from './Settings';
import DMManager from './twitch/DMManager';
import FeatureChecker from './FeatureChecker';
import GuildSettings from './settings/GuildSettings';
import Common from './common';
import { EmbedBuilder } from '@discordjs/builders';

/**
* The discord client handler and initializer of the bot
*/
export default class DiscordClient {
static _client: Client = new Client({
public static _client: Client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
Expand All @@ -31,44 +36,121 @@ export default class DiscordClient {
],
});

public static _safeGuilds: Guild[] = [];

constructor() {
new FeatureChecker();
new MessageHandler();
new ReactionHandler();
DiscordClient._client.on('ready', this.onReady.bind(this));
DiscordClient._client.on('threadCreate', this.onThreadCreate);
DiscordClient._client.on('guildCreate', this.onGuildJoin.bind(this));
DiscordClient._client.on('channelCreate', this.onChannelCreate.bind(this));
DiscordClient._client.on('channelDelete', this.onChannelDelete.bind(this));
DiscordClient._client.on('channelUpdate', this.onChannelUpdate.bind(this));
}

/**
* Logs the discord client into the discord api and starts the handlers
*/
login(): void {
DiscordClient._client
.login(process.env.DISCORD_TOKEN)
.catch(console.error)
.then((): void => {
if (Settings.settings['twitch-id'] !== '' && process.env.TWITCH_TOKEN)
this.startTwitch();
});
DiscordClient._client.login(process.env.DISCORD_TOKEN).catch(console.error);
}

/**
* Fires when the Discord bot is ready
*/
private async onReady(): Promise<void> {
await new FeatureChecker().check();
new MessageHandler();
new ReactionHandler();
if (this.twitchEnabled()) this.startTwitch();
this.joinAllThreads();
new TalkingChannel();
if (!DiscordClient._client.application?.owner)
await DiscordClient._client.application?.fetch().catch(console.error);
MessageHandler.addCommands();
if (Settings.settings.roles.length !== 0) new RoleChannelManager();
for (const guild of DiscordClient._safeGuilds) {
const mgr: RoleChannelManager = Common.getRoleChannelManager(guild);
if (await this.rolesEnabled(guild.id)) mgr.run();
}
}

/**
* Sets the bot up to be ready for a guild
* @param guild The guild the bot joined
*/
private async onGuildJoin(guild: Guild): Promise<void> {
new FeatureChecker().checkGuild(guild);
if (this.twitchEnabled()) new LiveChannel(guild);
const mgr: RoleChannelManager = Common.getRoleChannelManager(guild);
if (await this.rolesEnabled(guild.id)) mgr.run();
}

/**
* Checks if the created channel is a roles channel and retries executing the RolesChannelManager
* @param channel The channel that was created
*/
private async onChannelCreate(
channel: NonThreadGuildBasedChannel
): Promise<void> {
if (
channel.name === 'roles' &&
(await this.rolesEnabled(channel.guildId))
) {
const mgr: RoleChannelManager = Common.getRoleChannelManager(
channel.guild
);
if (mgr._channel === undefined) mgr.run();
}
}

/**
* Fires whenever a channel gets deleted
* @param channel The deleted channel
*/
private async onChannelDelete(
channel: DMChannel | NonThreadGuildBasedChannel
): Promise<void> {
if (!channel.isDMBased())
Common.getRoleChannelManager(channel.guild).onChannelDelete(channel);
}

/**
* Fires whenever there is a channel update (e. g. renaming)
* @param oldChannel The old channel
* @param newChannel The new channel
*/
private async onChannelUpdate(
oldChannel: DMChannel | NonThreadGuildBasedChannel,
newChannel: DMChannel | NonThreadGuildBasedChannel
): Promise<void> {
if (!oldChannel.isDMBased() && !newChannel.isDMBased())
Common.getRoleChannelManager(oldChannel.guild).onChannelUpdate(
oldChannel,
newChannel
);
}

/**
* Checks if the bot has been set up correctly to use Twitch
* @returns Whether Twitch is ready to be used
*/
private twitchEnabled(): boolean {
return Settings.settings['twitch-id'] !== '' && !!process.env.TWITCH_TOKEN;
}

/**
* Checks if specified guild has roles enabled
* @param guildId The guild id to check
* @returns Whether roles are enabled in the guild
*/
private async rolesEnabled(guildId: string): Promise<boolean> {
return (await GuildSettings.settings(guildId)).roles.length !== 0;
}

/**
* Joins all threads to be available there
*/
private joinAllThreads(): void {
for (const guild of DiscordClient._client.guilds.cache.toJSON())
for (const guild of DiscordClient._safeGuilds)
for (const threadChannel of (
guild.channels.cache.filter(
(channel: GuildChannel | ThreadChannel): boolean =>
Expand All @@ -86,7 +168,7 @@ export default class DiscordClient {
* Starts doing stuff with Twitch for the #live channel and DM notification handling
*/
private startTwitch(): void {
new LiveChannel();
for (const guild of DiscordClient._safeGuilds) new LiveChannel(guild);
new DMManager();
}

Expand Down
58 changes: 47 additions & 11 deletions src/FeatureChecker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Guild } from 'discord.js';
import DiscordClient from './DiscordClient';
import Settings from './Settings';
import GuildSettings from './settings/GuildSettings';
import { GuildInfo } from './settings/SettingsDB';

/**
* Checks what features are enabled in the settings
Expand All @@ -14,22 +18,17 @@ export default class FeatureChecker {
*/
#crash: boolean = false;

constructor() {
public async check() {
if (Settings.settings.logging === 'verbose')
this.status('Logging set to verbose');
if (Settings.settings['permission-roles'].length === 0)
this.warning('No permitted roles set (field "permission-roles" empty)');
if (Settings.settings.roles.length === 0) this.status('Roles disabled');
else this.status('Roles enabled');
for (const guild of DiscordClient._client.guilds.cache.toJSON())
await this.checkGuild(guild);

if (Settings.settings['twitch-id'])
if (process.env.TWITCH_TOKEN) this.status('Twitch enabled');
else this.warning('No Twitch token provided');
else if (process.env.TWITCH_TOKEN) this.warning('No Twitch ID provided');
else this.status('Twitch disabled');
if (Settings.settings.roles.length > 25)
this.error(
'Currently, only a maximum of 25 roles is allowed. If you need more, file an issue at https://github.com/TrojanerHD/TrojanerBot/issues/new'
);
if (
process.argv[
process.argv.findIndex((value: string): boolean => value === '-r') + 1
Expand All @@ -42,6 +41,42 @@ export default class FeatureChecker {
if (this.#crash) process.exit(1);
}

public async checkGuild(guild: Guild): Promise<void> {
const guildInfo: string = `for guild ${guild.id} (${guild.name})`;
let error: boolean = false;
try {
const info: GuildInfo = await GuildSettings.settings(guild.id);
if (info.permissionRoles.length === 0)
this.status(`No permitted roles set for ${guildInfo}`);
if (info.roles.length === 0) this.status(`Roles disabled ${guildInfo}`);
else this.status(`Roles enabled ${guildInfo}`);
if (info.roles.length > 25) {
error = true;
this.error(
`Currently, only a maximum of 25 roles is allowed. Guild ${guild.id} (${guild.name}) has more than 25 roles. If you need more, file an issue at https://github.com/TrojanerHD/TrojanerBot/issues/new`,
false
);
}
} catch (e: unknown) {
error = true;
this.error(
`Could not load information ${guildInfo} with reason ${e}`,
false
);
} finally {
if (!error) {
DiscordClient._safeGuilds.push(guild);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if safe guilds doesn't already have this guild to make this function safe when re-executing

if (!GuildSettings.settings(guild.id))
await GuildSettings.saveSettings(guild, {
permissionRoles: [],
roles: [],
streamers: [],
refreshToken: '',
}).catch(console.error);
} else this.warning(`Limited features due to problems ${guildInfo}`);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove a guild from the safe guilds if there was a problem to make this function safe when re-executing

}
}

/**
* Appends a warning to the log output
* @param message The warning message to append
Expand All @@ -63,9 +98,10 @@ export default class FeatureChecker {
/**
* Appends an error to the log output and will crash the bot
* @param message The error message to append
* @param crash Whether to crash the bot
*/
private error(message: string): void {
private error(message: string, crash: boolean = true): void {
this.#message += `Error: ${message}\n`;
this.#crash = true;
if (!this.#crash) this.#crash = crash;
}
}
36 changes: 23 additions & 13 deletions src/ReactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
import DiscordClient from './DiscordClient';
import assignRoles from './roles/assignRoles';
import MessageHandler from './messages/MessageHandler';
import Settings, { RolesField } from './Settings';
import { RolesField } from './settings/SettingsDB';
import { ActionRowBuilder, ButtonBuilder } from '@discordjs/builders';
import GuildSettings from './settings/GuildSettings';

/**
* Handles reactions (button presses, slash-commands)
Expand All @@ -26,14 +27,16 @@ export default class ReactionHandler {
* Fires whenever an interaction has been made
* @param interaction The interaction that has happened
*/
private onReaction(interaction: Interaction): void {
private async onReaction(interaction: Interaction): Promise<void> {
// Checks whether the interaction was a button
if (interaction.isButton()) {
// If the id of the button is a role name, the interaction's origin is from the role picker
const settingsRole: RolesField | undefined = Settings.settings.roles.find(
(role: RolesField): boolean =>
role.name.toLowerCase() === interaction.customId
);
const settingsRole: RolesField | undefined = interaction.guildId
? (await GuildSettings.settings(interaction.guildId)).roles.find(
(role: RolesField): boolean =>
role.name.toLowerCase() === interaction.customId
)
: undefined;
if (settingsRole !== undefined) {
// Hack to not reply with anything
interaction.reply({}).catch((reason: any): void => {
Expand Down Expand Up @@ -62,7 +65,8 @@ export default class ReactionHandler {
interaction
.reply({
content: 'Select your roles',
components: this.generateRoleSelectorComponent(
components: await this.generateRoleSelectorComponent(
interaction.guildId,
interaction.member as GuildMember | null
),
ephemeral: true,
Expand All @@ -81,19 +85,24 @@ export default class ReactionHandler {

/**
* Generates a role selector component with all selectable roles
* @param guild The guild the role selector is being generated for
* @param member The guild member the selector is created for
TrojanerHD marked this conversation as resolved.
Show resolved Hide resolved
* @returns A message action row array containing all selectable roles
*/
private generateRoleSelectorComponent(
private async generateRoleSelectorComponent(
guild: string | null,
member: GuildMember | null
): ActionRowBuilder<ButtonBuilder>[] {
): Promise<ActionRowBuilder<ButtonBuilder>[]> {
const messageActionRows: ActionRowBuilder<ButtonBuilder>[] = [];
if (guild === null) return [];

const roles: RolesField[] = (await GuildSettings.settings(guild)).roles;
// Every row can contain up to five roles
for (let i = 0; i < Settings.settings.roles.length / 5; i++) {
for (let i = 0; i < roles.length / 5; i++) {
// Add the current five roles as component
const currentMessageActionRow: ActionRowBuilder<ButtonBuilder> =
new ActionRowBuilder<ButtonBuilder>().addComponents(
// Add the current five roles as component
Settings.settings.roles.slice(i * 5, i * 5 + 5).map(
roles.slice(i * 5, i * 5 + 5).map(
// Map the roles stored in settings
(role: RolesField): ButtonBuilder =>
new ButtonBuilder()
Expand Down Expand Up @@ -127,7 +136,8 @@ export default class ReactionHandler {
interaction
.editReply({
content: 'Select your roles',
components: this.generateRoleSelectorComponent(
components: await this.generateRoleSelectorComponent(
interaction.guildId,
interaction.member as GuildMember | null
),
})
Expand Down
Loading
Loading