Compare commits

...

8 Commits
master ... dev

Author SHA1 Message Date
Rauf c7b55b1aae push time 2020-02-01 18:23:36 -05:00
Rauf 07029ffc52 feat: beginning of moderation 2020-01-25 12:02:34 -05:00
Rauf 2e3e0bbe0a feat(bot): added eval and connected mongodb 2020-01-19 13:55:35 -05:00
Rauf a62c0e3b78 feat(events): command user usage level checking 2020-01-14 00:15:34 -05:00
Rauf b12269f132 feat(cmd): bot information command 2020-01-14 00:04:17 -05:00
Rauf 685b1c1d39 fix(cmd): help command rewrite 2020-01-13 23:42:02 -05:00
Rauf bf24b9f46b feat: added events and commands 2020-01-13 17:38:12 -05:00
Rauf 2fb4842994 feat: initialized bot 2020-01-12 23:10:56 -05:00
48 changed files with 3874 additions and 2 deletions

4
.gitignore vendored
View File

@ -107,3 +107,7 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Config Files
src/config/*
build/*

2275
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,44 @@
"name": "lifeguard",
"version": "0.0.1-alpha",
"description": "Lifeguard Bot",
"main": "dist/bot.js",
"main": "build/src/index.js",
"repository": "https://gitdab.com/lifeguard/bot.git",
"author": "Rauf",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"@types/bson": "^4.0.1",
"@types/node": "^10.17.13",
"@types/ws": "^6.0.4",
"bson": "^4.0.3",
"gts": "^1.1.2",
"typescript": "^3.7.5"
},
"scripts": {
"check": "gts check",
"clean": "gts clean",
"compile": "tsc -p .",
"watch": "tsc -w -p .",
"fix": "gts fix",
"prepare": "npm.cmd run compile",
"pretest": "npm.cmd run compile",
"posttest": "npm.cmd run check"
},
"dependencies": {
"@types/mongodb": "^3.3.14",
"discord.js": "github:discordjs/discord.js",
"module-alias": "^2.2.2",
"mongodb": "^3.5.2",
"typy": "^3.3.0"
},
"_moduleAliases": {
"@lifeguard": "./build/src",
"@lifeguard/base": "./",
"@assertions": "./build/src/assertions",
"@config": "./build/src/config",
"@events": "./build/src/events",
"@models": "./build/src/models",
"@plugins": "./build/src/plugins",
"@structures": "./build/src/structures",
"@util": "./build/src/util"
}
}

4
prettier.config.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
singleQuote: true,
trailingComma: 'es5',
};

20
src/PluginClient.ts Normal file
View File

@ -0,0 +1,20 @@
import { name, url } from '@config/mongodb';
import { Plugin } from '@plugins/Plugin';
import { Database } from '@util/Database';
import { Client, ClientOptions, Collection } from 'discord.js';
export class PluginClient extends Client {
plugins!: Collection<string, Plugin>;
db: Database;
constructor(options?: ClientOptions) {
super(options);
this.db = new Database({
name,
url,
MongoOptions: {
useNewUrlParser: true,
useUnifiedTopology: true,
},
});
}
}

View File

@ -0,0 +1,15 @@
import { GuildMember, Guild } from 'discord.js';
import { developers } from '@config/bot';
export function calcUserLevel(user: GuildMember, guild: Guild) {
if (developers.includes(user.id)) {
return 5;
}
if (user.id === guild.ownerID) {
return 3;
}
if (user.permissions.has('ADMINISTRATOR', true)) {
return 2;
}
return 0;
}

8
src/events/Event.ts Normal file
View File

@ -0,0 +1,8 @@
import { PluginClient } from '../PluginClient';
// tslint:disable-next-line: no-any
type EventFunc = (lifeguard: PluginClient, ...args: any[]) => void;
export class Event {
constructor(public name: string, public func: EventFunc) {}
}

View File

@ -0,0 +1,35 @@
import { Event } from '@events/Event';
import { GuildStructure } from '@structures/GuildStructure';
import { defaultEmbed } from '@util/DefaultEmbed';
import { GuildChannel, TextChannel } from 'discord.js';
export const event = new Event(
'channelCreate',
async (lifeguard, channel: GuildChannel) => {
const dbGuild = await (channel.guild as GuildStructure).db;
if (dbGuild?.config.channels?.logging) {
const modlog = channel.guild.channels.get(
dbGuild.config.channels.logging
) as TextChannel;
const auditLog = await channel.guild.fetchAuditLogs({
type: 'CHANNEL_CREATE',
});
const auditLogEntry = auditLog.entries.last();
// const embed = defaultEmbed()
// .setDescription(
// `:pencil: Channel ${channel.toString()} (${channel.id}) was created`
// )
// .setTitle('Channel Create');
// if (auditLogEntry) {
// embed.setAuthor(`${auditLogEntry.executor.tag}`);
// }
const embed = defaultEmbed();
modlog.send(embed);
}
}
);

21
src/events/eventLoader.ts Normal file
View File

@ -0,0 +1,21 @@
import { Event } from '@events/Event';
import { PluginClient } from '@lifeguard/PluginClient';
import { readdir } from 'fs';
import { promisify } from 'util';
const readDir = promisify(readdir);
export async function EventLoader(lifeguard: PluginClient) {
const eventFiles = await readDir('./build/src/events');
for await (const file of eventFiles) {
if (file.endsWith('js') && file !== 'Event.js') {
const { event } = require(`./${file}`);
if (event instanceof Event) {
lifeguard.on(event.name, (...args: []) => {
event.func(lifeguard, ...args);
});
}
}
}
}

18
src/events/guildCreate.ts Normal file
View File

@ -0,0 +1,18 @@
import { Event } from '@events/Event';
import { Guild } from 'discord.js';
export const event = new Event(
'guildCreate',
async (lifeguard, guild: Guild) => {
await lifeguard.db.guilds.insertOne({ id: guild.id, config: {} });
if (lifeguard.user) {
lifeguard.user.setPresence({
activity: {
name: `${lifeguard.users.size} people in the pool`,
type: 'WATCHING',
},
status: 'online',
});
}
}
);

View File

@ -0,0 +1,49 @@
import { calcUserLevel } from '@assertions/userLevel';
import { prefix } from '@config/bot';
import { Event } from '@events/Event';
import { PluginClient } from '@lifeguard/PluginClient';
import { UserDoc } from '@models/User';
import { Message } from 'discord.js';
function parseContent(content: string) {
const split = content.split(' ');
const cmdName = split[0].slice(prefix.length);
split.shift();
return [cmdName, ...split];
}
function getCommandFromPlugin(lifeguard: PluginClient, cmdName: string) {
const plugin = lifeguard.plugins.find(p => p.has(cmdName));
if (plugin) {
return plugin?.get(cmdName);
} else {
const plugins = [...lifeguard.plugins.values()];
const cmds = plugins
.map(p => [...p.values()])
.reduce((acc, val) => acc.concat(val), []);
return cmds.find(cmd => cmd.options.alias?.includes(cmdName));
}
}
export const event = new Event(
'lifeguardCommandUsed',
async (lifeguard, msg: Message, dbUser: UserDoc) => {
if (msg.author.bot) {
return;
}
if (dbUser.blacklisted) {
return;
}
const [cmdName, ...args] = parseContent(msg.content);
const cmd = getCommandFromPlugin(lifeguard, cmdName);
if (cmd) {
if (msg.member && msg.guild) {
const userLevel = calcUserLevel(msg.member, msg.guild);
if (userLevel >= cmd.options.level) {
cmd.func(lifeguard, msg, args, dbUser);
}
}
}
}
);

43
src/events/message.ts Normal file
View File

@ -0,0 +1,43 @@
import { prefix } from '@config/bot';
import { Event } from '@events/Event';
import { Guild } from '@models/Guild';
import { User } from '@models/User';
import { GuildStructure } from '@structures/GuildStructure';
import { Message } from 'discord.js';
export const event = new Event('message', async (lifeguard, msg: Message) => {
let dbUser = await lifeguard.db.users.findOne({
id: msg.author.id,
});
if (!dbUser) {
await lifeguard.db.users.insertOne(new User({ id: msg.author.id }));
dbUser = await lifeguard.db.users.findOne({
id: msg.author.id,
});
}
if (msg.guild) {
const dbGuild = await (msg.guild as GuildStructure).db;
if (!dbGuild) {
await lifeguard.db.guilds.insertOne(new Guild({ id: msg.guild.id }));
}
}
if (dbUser) {
dbUser.stats.totalSentMessages++;
dbUser.stats.totalSentCharacters += msg.content.length;
dbUser.stats.totalCustomEmojisUsed +=
msg.content.match(/<.[^ ]*>/)?.length ?? 0;
dbUser.stats.totalTimesMentionedAUser += msg.mentions.users.size;
dbUser.stats.totalSentAttachments += msg.attachments.size;
await lifeguard.db.users.updateOne(
{ id: dbUser.id },
{ $set: { stats: dbUser.stats } }
);
}
if (msg.content.startsWith(prefix)) {
lifeguard.emit('lifeguardCommandUsed', msg, dbUser);
}
});

View File

@ -0,0 +1,20 @@
import { Event } from '@events/Event';
import { GuildStructure } from '@structures/GuildStructure';
import { MessageReaction, User } from 'discord.js';
export const event = new Event(
'messageReactionAdd',
async (lifeguard, reaction: MessageReaction, user: User) => {
await lifeguard.db.users.updateOne(
{ id: user.id },
{ $inc: { 'stats.totalTimesReacted': 1 } }
);
const dbGuild = await (reaction.message.guild as GuildStructure).db;
if (
dbGuild?.config.channels?.starboard &&
reaction.emoji.name === dbGuild?.config.starboard?.emoji
) {
lifeguard.emit('starboardReactionAdd', reaction);
}
}
);

14
src/events/ready.ts Normal file
View File

@ -0,0 +1,14 @@
import { Event } from '@events/Event';
export const event = new Event('ready', lifeguard => {
console.log('Connected to Discord');
if (lifeguard.user) {
lifeguard.user.setPresence({
activity: {
name: `${lifeguard.users.size} people in the pool`,
type: 'WATCHING',
},
status: 'online',
});
}
});

View File

@ -0,0 +1,64 @@
import { Event } from '@events/Event';
import { GuildStructure } from '@structures/GuildStructure';
import { defaultEmbed } from '@util/DefaultEmbed';
import { TextChannel, MessageReaction } from 'discord.js';
export const event = new Event(
'starboardReactionAdd',
async (lifeguard, reaction: MessageReaction) => {
const dbGuild = await (reaction.message.guild as GuildStructure).db;
if (dbGuild?.config.starboard && dbGuild.config.channels?.starboard) {
const starboardChannel = reaction.message.guild?.channels.get(
dbGuild.config.channels.starboard
) as TextChannel;
const starboard = dbGuild.config.starboard;
if (
starboardChannel &&
!starboard.ignoredChannels.includes(reaction.message.channel.id)
) {
if (reaction.count ?? 0 >= starboard.minCount) {
const starboardMessage = starboard.messages.find(
m => m.id === reaction.message.id
);
if (starboardMessage) {
const starboardMessageInChannel = starboardChannel.messages.get(
starboardMessage.starboardID
);
starboardMessage.count = reaction.count ?? starboardMessage.count;
const embed = defaultEmbed()
.setAuthor(
starboardMessageInChannel?.author.tag,
starboardMessageInChannel?.author.avatarURL() ?? ''
)
.setDescription(reaction.message.content);
starboardMessageInChannel?.edit(
`${starboard.emoji} ${reaction.count} ${reaction.message.channel} (${reaction.message.id})`,
embed
);
} else {
const embed = defaultEmbed()
.setAuthor(
reaction.message.author.tag,
reaction.message.author.avatarURL() ?? ''
)
.setDescription(reaction.message.content);
const starboardMessage = await starboardChannel.send(
`${starboard.emoji} ${reaction.count} ${reaction.message.channel} (${reaction.message.id})`,
embed
);
starboard.messages.push({
id: reaction.message.id,
starboardID: starboardMessage.id,
content: reaction.message.content,
count: reaction.count ?? 0,
});
}
await lifeguard.db.guilds.updateOne(
{ id: dbGuild.id },
{ $set: { 'config.starboard': starboard } }
);
}
}
}
}
);

29
src/index.ts Normal file
View File

@ -0,0 +1,29 @@
import 'module-alias/register';
import { token } from '@config/bot';
import { EventLoader } from '@events/eventLoader';
import { PluginClient } from '@lifeguard/PluginClient';
import { PluginLoader } from '@plugins/pluginLoader';
import { StructureLoader } from '@structures/structureLoader';
StructureLoader();
const lifeguard = new PluginClient();
EventLoader(lifeguard);
PluginLoader().then(plugins => {
lifeguard.plugins = plugins;
});
lifeguard.db
.connect()
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error(err));
lifeguard.login(token).then(() => {
if (lifeguard.user) {
console.log(
`Logged in to ${lifeguard.user.username}#${lifeguard.user.discriminator}`
);
}
});

52
src/models/Guild.ts Normal file
View File

@ -0,0 +1,52 @@
import { ObjectId } from 'bson';
export interface GuildChannels {
logging?: string;
starboard?: string;
}
export interface GuildRoles {
[key: string]: string | string[] | undefined;
muted?: string;
moderator?: string;
groupRoles?: string[];
lockedRoles?: string[];
}
export interface GuildStarboardMessage {
id: string;
starboardID: string;
count: number;
content: string;
}
export interface GuildStarboard {
emoji: string;
minCount: number;
ignoredChannels: string[];
messages: GuildStarboardMessage[];
}
export interface GuildConfig {
blacklisted?: boolean;
channels?: GuildChannels;
roles?: GuildRoles;
starboard?: GuildStarboard;
}
export interface GuildDoc {
id: string;
config?: GuildConfig;
}
export class Guild implements GuildDoc {
_id: ObjectId;
id: string;
config: GuildConfig;
constructor(data: GuildDoc) {
this._id = new ObjectId();
this.id = data.id;
this.config = data.config ?? {};
this.config.blacklisted = data.config?.blacklisted ?? false;
}
}

78
src/models/User.ts Normal file
View File

@ -0,0 +1,78 @@
import { ObjectId } from 'bson';
import { PermissionOverwriteOption } from 'discord.js';
export interface UserInfraction {
action: 'Warn' | 'Kick' | 'Mute' | 'Ban';
active: boolean;
guild: string;
id: number;
moderator: string;
reason: string;
time: Date;
}
export interface UserBackup {
guild: string;
id: number;
roles: string[];
nickname: string;
channelOverrides: UserBackupChannelOverride[];
deafened: boolean;
muted: boolean;
createdAt: Date;
}
export interface UserBackupChannelOverride {
channelId: number;
overrides: PermissionOverwriteOption;
}
export interface UserStats {
totalSentMessages: number;
totalSentCharacters: number;
totalDeletedMessages: number;
totalCustomEmojisUsed: number;
totalTimesMentionedAUser: number;
totalSentAttachments: number;
totalTimesReacted: number;
mostUsedReaction: string;
}
export interface UserReactions {
[name: string]: number;
}
export interface UserDoc {
blacklisted?: boolean;
id: string;
infractions?: UserInfraction[];
backups?: UserBackup[];
stats?: UserStats;
reactions?: UserReactions;
}
export class User implements UserDoc {
_id: ObjectId;
blacklisted: boolean;
id: string;
infractions: UserInfraction[];
backups: UserBackup[];
stats: UserStats;
constructor(data: UserDoc) {
this._id = new ObjectId();
this.blacklisted = data.blacklisted ?? false;
this.id = data.id;
this.infractions = data.infractions ?? [];
this.backups = data.backups ?? [];
this.stats = data.stats ?? {
totalSentMessages: 0,
totalSentCharacters: 0,
totalDeletedMessages: 0,
totalCustomEmojisUsed: 0,
totalTimesMentionedAUser: 0,
totalSentAttachments: 0,
totalTimesReacted: 0,
mostUsedReaction: '',
};
}
}

27
src/plugins/Command.ts Normal file
View File

@ -0,0 +1,27 @@
import { Message, PermissionString } from 'discord.js';
import { PluginClient } from '@lifeguard/PluginClient';
import { UserDoc } from '@models/User';
type CommandFunction = (
lifeguard: PluginClient,
msg: Message,
args: string[],
dbUser?: UserDoc
) => void;
interface CommandOptions {
alias?: string[];
guildOnly?: boolean;
hidden?: boolean;
level: number;
usage: string[];
permissions?: PermissionString[];
}
export class Command {
constructor(
public name: string,
public func: CommandFunction,
public options: CommandOptions
) {}
}

8
src/plugins/Plugin.ts Normal file
View File

@ -0,0 +1,8 @@
import { Collection } from 'discord.js';
import { Command } from '@plugins/Command';
export class Plugin extends Collection<string, Command> {
constructor() {
super();
}
}

View File

@ -0,0 +1,46 @@
import { Command } from '@plugins/Command';
import { t as typy } from 'typy';
export const command = new Command(
'config',
async (lifeguard, msg, [cmd, ...args]) => {
const path = args[0];
switch (cmd) {
case 'get':
const guild = await lifeguard.db.guilds.findOne({ id: msg.guild?.id });
if (guild) {
const config = guild['config'];
if (path) {
msg.channel.send(`${path} - ${typy(config, path).safeObject}`);
} else {
msg.channel.send(
`\`\`\`json\n${JSON.stringify(config, null, 2)}\`\`\``
);
}
}
break;
case 'set':
const res = await lifeguard.db.guilds.findOneAndUpdate(
{
id: msg.guild?.id,
},
{
$set: { [`config.${path}`]: JSON.parse(args[1]) },
}
);
if (res.ok) {
msg.channel.send('Value has been set successfully');
}
break;
default:
break;
}
},
{
level: 3,
usage: ['config get', 'config get {key}', 'config set {key} {value}'],
hidden: true,
}
);

38
src/plugins/admin/role.ts Normal file
View File

@ -0,0 +1,38 @@
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'role',
async (lifeguard, msg, args) => {
const [cmd, uid, rid, ...r] = args;
const u = parseUser(uid);
const role = msg.guild?.roles.get(rid);
switch (cmd) {
case 'add':
if (role) {
const member = msg.guild?.members.get(u);
member?.roles.add(role, r.join(' '));
msg.channel.send(`Added ${role.name} to ${member}`);
}
break;
case 'rmv':
if (role) {
const member = msg.guild?.members.get(u);
member?.roles.remove(role, r.join(' '));
msg.channel.send(`Removed ${role.name} from ${member}`);
}
break;
default:
break;
}
},
{
level: 1,
usage: [
'role add {user} {role id} [reason]',
'role rmv {user} {role id} [reason]',
],
}
);

View File

@ -0,0 +1,31 @@
import { Command } from '@plugins/Command';
export const command = new Command(
'roles',
async (lifeguard, msg) => {
const roleList = msg.guild?.roles
?.sort((ra, rb) => rb.position - ra.position)
.map(r => `${r.id} - ${r.name} (${r.members.size} members)`);
const blocks: string[] = [''];
roleList?.forEach(r => {
let currentBlockIndex: number = blocks.length - 1;
if (
blocks[currentBlockIndex].length > 1990 ||
blocks[currentBlockIndex].concat(`\n${r}`).length > 1990
) {
blocks.push('');
currentBlockIndex++;
}
blocks[currentBlockIndex] += `\n${r}`;
});
blocks.forEach(b => msg.channel.send(`\`\`\`dns${b}\`\`\``));
},
{
level: 1,
usage: ['roles'],
}
);

View File

@ -0,0 +1,33 @@
import { Command } from '@plugins/Command';
import { TextChannel } from 'discord.js';
export const command = new Command(
'slowmode',
async (lifeguard, msg, args) => {
const [cmd, time] = args;
switch (cmd) {
case 'set':
if (msg.guild) {
(msg.channel as TextChannel).setRateLimitPerUser(+time);
msg.channel.send(
`Slowmode has been set to 1 message every ${time} seconds`
);
}
break;
case 'off':
if (msg.guild) {
(msg.channel as TextChannel).setRateLimitPerUser(0);
msg.channel.send(`Slowmode has been turned off`);
}
break;
default:
break;
}
},
{
level: 1,
usage: ['slowmode set {time}', 'slowmode off'],
}
);

28
src/plugins/debug/ping.ts Normal file
View File

@ -0,0 +1,28 @@
import { Command } from '@plugins/Command';
import { defaultEmbed } from '@util/DefaultEmbed';
export const command = new Command(
'ping',
async (lifeguard, msg, args) => {
const m = await msg.channel.send('Ping?');
m.delete({ timeout: 100 });
const embed = defaultEmbed()
.setTitle('Pong! :ping_pong:')
.addField('Bot Latency', `${Math.round(lifeguard.ws.ping)}ms`)
.addField(
'Message Latency',
`${m.createdTimestamp - msg.createdTimestamp}ms`
)
.setFooter(
`Executed By ${msg.author.tag}`,
msg.author.avatarURL() ?? msg.author.defaultAvatarURL
);
msg.channel.send(embed);
},
{
level: 0,
usage: ['ping'],
}
);

76
src/plugins/dev/eval.ts Normal file
View File

@ -0,0 +1,76 @@
import { Command } from '@plugins/Command';
import { defaultEmbed } from '@util/DefaultEmbed';
import { inspect } from 'util';
import { runInNewContext } from 'vm';
function parseBlock(script: string) {
const cbr = /^(([ \t]*`{3,4})([^\n]*)([\s\S]+?)(^[ \t]*\2))/gm;
const result = cbr.exec(script);
if (result) {
return result[4];
}
return script;
}
async function run(
script: string,
ctx: object,
opts: object
): Promise<string | Error> {
try {
const result = await runInNewContext(
`(async () => { ${script} })()`,
ctx,
opts
);
if (typeof result !== 'string') {
return inspect(result);
}
return result;
} catch (err) {
return err;
}
}
function makeCodeBlock(data: string, lang?: string) {
return `\`\`\`${lang}\n${data}\n\`\`\``;
}
export const command = new Command(
'eval',
async (lifeguard, msg, args, dbUser) => {
const start = Date.now();
const script = parseBlock(args.join(' '));
const exec = await run(
script,
{
lifeguard,
msg,
defaultEmbed,
dbUser,
},
{ filename: msg.guild?.id.toString() }
);
const end = Date.now();
if (typeof exec === 'string') {
const embed = defaultEmbed()
.addField('Input', makeCodeBlock(script, 'js'))
.addField('Output', makeCodeBlock(exec, 'js'))
.setFooter(`Script Executed in ${end - start}ms`);
msg.channel.send(embed);
} else {
const embed = defaultEmbed()
.addField('Input', makeCodeBlock(script, 'js'))
.addField('Output', makeCodeBlock(`${exec.name}: ${exec.message}`))
.setFooter(`Script Executed in ${end - start}ms`);
msg.channel.send(embed);
}
},
{
level: 5,
usage: ['eval {code}'],
}
);

View File

@ -0,0 +1,23 @@
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'blacklist',
async (lifeguard, msg, args) => {
const u = parseUser(args[0]);
try {
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $set: { blacklisted: true } },
{ returnOriginal: false }
);
msg.channel.send(`<@${u}> was sucessfully blacklisted`);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 4,
usage: ['blacklist {user}'],
}
);

51
src/plugins/info/about.ts Normal file
View File

@ -0,0 +1,51 @@
import { Command } from '@plugins/Command';
import { exec } from 'child_process';
import { MessageEmbed } from 'discord.js';
import { resolve } from 'path';
import { promisify } from 'util';
export const command = new Command(
'about',
async (lifeguard, msg, args) => {
// Convert child_process#exec to async/await
const run = promisify(exec);
// Get Git Commit ID
const { stdout: gitCommitID } = await run('git rev-parse HEAD', {
cwd: resolve(__dirname),
});
const gitCommitURL = `https://gitdab.com/lifeguard/bot/commit/${gitCommitID}`;
// Get Node Version
const { stdout: nodeVersion } = await run('node -v');
// Get Discord.js Version
const { version: discordjsVersion } = require('discord.js/package.json');
// Get Lifeguard Version
const {
version: lifeguardVersion,
} = require('@lifeguard/base/package.json');
const embed = new MessageEmbed()
.setTitle('About Lifeguard')
.addField(
'Git Commit',
`[${gitCommitID}](${gitCommitURL}) (https://gitdab.com/lifeguard/bot/)`
)
.addField('Node Version', nodeVersion)
.addField('Discord.js Version', discordjsVersion)
.addField('Lifeguard Version', lifeguardVersion)
.setColor(0x7289da)
.setFooter(
`Executed By ${msg.author.tag}`,
msg.author.avatarURL() ?? msg.author.defaultAvatarURL
)
.setTimestamp();
msg.channel.send(embed);
},
{
level: 0,
usage: ['about'],
}
);

77
src/plugins/info/help.ts Normal file
View File

@ -0,0 +1,77 @@
import { calcUserLevel } from '@assertions/userLevel';
import { Command } from '@plugins/Command';
import { Plugin } from '@plugins/Plugin';
import { defaultEmbed } from '@util/DefaultEmbed';
import { Collection, Guild, GuildMember } from 'discord.js';
function convertPlugins(
plugins: Collection<string, Plugin>,
member: GuildMember,
guild: Guild
) {
return plugins
.map((plugin, key) => ({
name: key,
cmds: [...plugin.values()]
.filter(cmd => !cmd.options.hidden)
.filter(cmd => calcUserLevel(member, guild) >= cmd.options.level)
.map(cmd => cmd.name)
.sort((a, b) => a.localeCompare(b)),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
export const command = new Command(
'help',
(lifeguard, msg, args) => {
if (!args.length) {
const plugins = convertPlugins(
lifeguard.plugins,
msg.member as GuildMember,
msg.guild as Guild
);
const embed = defaultEmbed()
.setTitle('Lifeguard Help')
.setFooter(
`Executed By ${msg.author.tag}`,
msg.author.avatarURL() ?? msg.author.defaultAvatarURL
);
for (const plugin of plugins) {
if (plugin.cmds.length > 0) {
embed.addField(plugin.name, plugin.cmds.join('\n'));
}
}
msg.channel.send(embed);
} else {
const plugin = lifeguard.plugins.find(plugin => plugin.has(args[0]));
const cmd = plugin?.get(args[0]);
if (cmd) {
const embed = defaultEmbed()
.setTitle(cmd.name)
.setFooter(
`Executed By ${msg.author.tag}`,
msg.author.avatarURL() ?? msg.author.defaultAvatarURL
);
const options = Object.entries(cmd.options);
options.map(([key, val]) => {
if (key === 'usage') {
embed.addField(key, val.join('\n'));
return;
}
embed.addField(key, `${val}`);
});
msg.channel.send(embed);
}
}
},
{
level: 0,
usage: ['help', 'help [name]'],
}
);

View File

@ -0,0 +1,55 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'ban',
async (lifeguard, msg, [uid, ...reason]) => {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infraction
const inf: UserInfraction = {
action: 'Ban',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Get User
const member = await msg.guild?.members.get(u);
// Notify User about Action
member?.send(
`You have been banned from **${msg.guild?.name}** for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
// Ban User
member?.ban({ reason: reason.join(' ') });
// Tell moderator action was successful
msg.channel.send(
`${member?.user.tag} was banned by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 1,
usage: ['ban {user} [reason]'],
}
);

View File

@ -0,0 +1,60 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
import { GuildMember, User } from 'discord.js';
export const command = new Command(
'forceban',
async (lifeguard, msg, [uid, ...reason]) => {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infraction
const inf: UserInfraction = {
action: 'Ban',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Ban user from guild
const member = await msg.guild?.members.ban(u, {
reason: reason.join(' '),
});
// Retreive user tag
let tag;
if (member instanceof GuildMember) {
tag = member.user.tag;
} else if (member instanceof User) {
tag = member.tag;
} else {
tag = member;
}
// Tell moderator ban was successful
msg.channel.send(
`${tag} was force-banned by ${msg.author.toString()} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 1,
usage: ['forceban {user} [reason]'],
}
);

View File

@ -0,0 +1,54 @@
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
import { MessageAttachment } from 'discord.js';
export const command = new Command(
'infractions',
async (lifeguard, msg, [cmd, ...args], dbUser) => {
switch (cmd) {
case 'archive':
// Get users from Database that have infractions
const dbUsers = lifeguard.db.users.find({
infractions: { $elemMatch: { guild: msg.guild?.id } },
});
const guildInfractions = (
await dbUsers
// Filter out infractions not from current guild
.map(u => u.infractions.filter(inf => inf.guild === msg.guild?.id))
.toArray()
)
// Flatten Array
.reduce((acc, val) => acc.concat(val), []);
// Send archive as JSON file
msg.channel.send(
new MessageAttachment(
Buffer.from(JSON.stringify(guildInfractions, null, 2)),
`${msg.guild?.id}.infractions.json`
)
);
break;
case 'info':
const [user, infID] = args;
const u = parseUser(user);
const dbUser = await lifeguard.db.users.findOne({ id: u });
const inf = dbUser?.infractions.find(inf => inf.id === +infID);
msg.channel.send(`\`\`\`json\n${JSON.stringify(inf, null, 2)}\n\`\`\``);
break;
default:
break;
}
},
{
level: 1,
usage: [
'infractions archive',
'infractions search {user}',
'infractions info {user} {id}',
],
alias: ['inf'],
}
);

View File

@ -0,0 +1,55 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'kick',
async (lifeguard, msg, [uid, ...reason]) => {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infraction
const inf: UserInfraction = {
action: 'Kick',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Get User
const member = await msg.guild?.members.get(u);
// Notify User about Action
member?.send(
`You have been kicked from **${msg.guild?.name}** for \`${reason.join(
' '
)}\``
);
// Ban the User
member?.kick(reason.join(' '));
// Tell moderator action was successful
msg.channel.send(
`${member?.user.tag} was kicked by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 1,
usage: ['kick {user} [reason]'],
}
);

View File

@ -0,0 +1,20 @@
import { Command } from '@plugins/Command';
import { command as ban } from '@plugins/moderation/ban';
export const command = new Command(
'mban',
async (lifeguard, msg, args) => {
// Find where '-r' is in the args
const reasonFlagIndex = args.indexOf('-r') || args.length;
// Get users from args
const users = args.slice(0, reasonFlagIndex);
// Get reason from args
const reason = args.slice(reasonFlagIndex + 1).join(' ');
// Run ban command for each user
users.forEach(user => ban.func(lifeguard, msg, [user, reason]));
},
{
level: 1,
usage: ['mban {users} -r [reason]'],
}
);

View File

@ -0,0 +1,20 @@
import { Command } from '@plugins/Command';
import { command as kick } from '@plugins/moderation/kick';
export const command = new Command(
'mkick',
async (lifeguard, msg, args) => {
// Find where '-r' is in the args
const reasonFlagIndex = args.indexOf('-r');
// Get users from args
const uids = args.slice(0, reasonFlagIndex);
// Get reason from args
const reason = args.slice(reasonFlagIndex + 1).join(' ');
// Run ban command for each user
uids.forEach(uid => kick.func(lifeguard, msg, [uid, reason]));
},
{
level: 1,
usage: ['mkick {users} -r [reason]'],
}
);

View File

@ -0,0 +1,59 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { defaultEmbed } from '@util/DefaultEmbed';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'mute',
async (lifeguard, msg, [uid, ...reason]) => {
// Get guild from db
const guild = await lifeguard.db.guilds.findOne({ id: msg.guild?.id });
// Check if muted role exists
if (guild?.config.roles?.muted) {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infracrion
const inf: UserInfraction = {
action: 'Mute',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Get User
const member = msg.guild?.members.get(u);
// Add role to user
await member?.roles.add(guild.config.roles.muted);
// Tell moderator action was successfull
msg.channel.send(
`${member?.user.tag} was muted by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
} else {
const embed = defaultEmbed()
.setTitle(':rotating_light: Error! :rotating_light:')
.setDescription('No mute role configured!');
msg.channel.send(embed);
}
},
{
level: 1,
usage: ['mute {user} [reason]'],
}
);

View File

@ -0,0 +1,57 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'softban',
async (lifeguard, msg, [uid, ...reason]) => {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infraction
const inf: UserInfraction = {
action: 'Ban',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Get User
const member = await msg.guild?.members.get(u);
// Notify user of action
member?.send(
`You have been soft-banned from **${msg.guild?.name}** for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}`
);
// Ban User
member?.ban({ reason: reason.join(' '), days: 7 });
// Unban User
await msg.guild?.members.unban(u, reason.join(' '));
// Tell moderator action was successfull
msg.channel.send(
`${member?.user.tag} was soft-banned by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}`
);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 1,
usage: ['softban {user} [reason]'],
}
);

View File

@ -0,0 +1,39 @@
import { Command } from '@plugins/Command';
import { defaultEmbed } from '@util/DefaultEmbed';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'unmute',
async (lifeguard, msg, [uid, ...reason]) => {
// Get guild from database
const guild = await lifeguard.db.guilds.findOne({ id: msg.guild?.id });
// Check if muted role exists
if (guild?.config.roles?.muted) {
// Parse user id from mention
const u = parseUser(uid);
try {
// Get User
const member = msg.guild?.members.get(u);
// Remove Role
await member?.roles.remove(guild.config.roles.muted);
// Tell moderator action was successfull
msg.channel.send(
`${member?.user.tag} was unmuted by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
} else {
const embed = defaultEmbed()
.setTitle(':rotating_light: Error! :rotating_light:')
.setDescription('No mute role configured!');
msg.channel.send(embed);
}
},
{
level: 1,
usage: ['unmute {user} [reason]'],
}
);

View File

@ -0,0 +1,47 @@
import { UserInfraction } from '@models/User';
import { Command } from '@plugins/Command';
import { parseUser } from '@util/parseUser';
export const command = new Command(
'warn',
async (lifeguard, msg, [uid, ...reason]) => {
// Parse user id from mention
const u = parseUser(uid);
try {
// Create Infracion
const inf: UserInfraction = {
action: 'Warn',
active: true,
guild: msg.guild?.id as string,
id: (await lifeguard.db.users.findOne({ id: u }))?.infractions
.length as number,
moderator: msg.author.id,
reason: reason.join(' '),
time: new Date(),
};
// Update User in Database
await lifeguard.db.users.findOneAndUpdate(
{ id: u },
{ $push: { infractions: inf } },
{ returnOriginal: false }
);
// Get User
const member = msg.guild?.members.get(u);
// Tell moderator action was sucessfull
msg.channel.send(
`${member?.user.tag} was warned by ${msg.author.tag} for \`${
reason.length > 0 ? reason.join(' ') : 'No Reason Specified'
}\``
);
} catch (err) {
msg.channel.send(err.message);
}
},
{
level: 1,
usage: ['warn {user} [reason]'],
}
);

View File

@ -0,0 +1,36 @@
import { promisify } from 'util';
import { readdir, lstat } from 'fs';
import { Collection } from 'discord.js';
import { Plugin } from './Plugin';
import { Command } from './Command';
export async function PluginLoader() {
const readDir = promisify(readdir);
const stats = promisify(lstat);
const plugins = new Collection<string, Plugin>();
const pluginDir = './build/src/plugins';
const folders = await readDir(pluginDir);
for await (const folder of folders) {
const folderDir = `${pluginDir}/${folder}`;
const info = await stats(folderDir);
if (info.isDirectory()) {
const plugin = new Plugin();
const files = await readDir(`${folderDir}`);
for await (const file of files) {
if (file.endsWith('.js')) {
const command = require(`./${folder}/${file}`).command;
if (command instanceof Command) {
plugin.set(command.name, command);
}
}
}
plugins.set(folder, plugin);
}
}
return plugins;
}

View File

@ -0,0 +1,29 @@
import { Structures, Guild, GuildMember } from 'discord.js';
import { PluginClient } from 'PluginClient';
import { inspect } from 'util';
Structures.extend('GuildMember', guildMember => {
return class extends guildMember {
_client: PluginClient;
constructor(client: PluginClient, data: object, guild: Guild) {
super(client, data, guild);
this._client = client;
}
get db() {
return this._client.db.users.findOne({ id: this.user.id });
}
};
});
export class GuildMemberStructure extends GuildMember {
_client: PluginClient;
constructor(client: PluginClient, data: object, guild: Guild) {
super(client, data, guild);
this._client = client;
}
get db() {
return this._client.db.users.findOne({ id: this.user.id });
}
}

View File

@ -0,0 +1,29 @@
import { Structures, Guild } from 'discord.js';
import { PluginClient } from 'PluginClient';
import { inspect } from 'util';
Structures.extend('Guild', guildClass => {
return class extends guildClass {
_client: PluginClient;
constructor(client: PluginClient, data: object) {
super(client, data);
this._client = client;
}
get db() {
return this._client.db.guilds.findOne({ id: this.id });
}
};
});
export class GuildStructure extends Guild {
_client: PluginClient;
constructor(client: PluginClient, data: object) {
super(client, data);
this._client = client;
}
get db() {
return this._client.db.guilds.findOne({ id: this.id });
}
}

View File

@ -0,0 +1,11 @@
import { readdirSync } from 'fs';
export function StructureLoader() {
const structureFiles = readdirSync('./build/src/structures');
for (const file of structureFiles) {
if (file !== 'structureLoader.js' && file.endsWith('js')) {
require(`./${file}`);
}
}
}

32
src/util/Database.ts Normal file
View File

@ -0,0 +1,32 @@
import { connect, Db, MongoClientOptions, Collection } from 'mongodb';
import { User } from '../models/User';
import { Guild } from '../models/Guild';
interface DatabaseConfig {
url: string;
name: string;
MongoOptions?: MongoClientOptions;
}
export class Database {
db!: Db;
constructor(protected config: DatabaseConfig) {}
async connect() {
const client = await connect(
this.config.url,
this.config.MongoOptions
).catch(err => {
throw err;
});
this.db = client.db(this.config.name);
}
get guilds(): Collection<Guild> {
return this.db.collection('guilds');
}
get users(): Collection<User> {
return this.db.collection('users');
}
}

5
src/util/DefaultEmbed.ts Normal file
View File

@ -0,0 +1,5 @@
import { MessageEmbed } from 'discord.js';
export function defaultEmbed() {
return new MessageEmbed().setColor(0x7289da).setTimestamp();
}

13
src/util/parseUser.ts Normal file
View File

@ -0,0 +1,13 @@
export function parseUser(user: string) {
if (user.startsWith("<@") && user.endsWith(">")) {
user = user.slice(2, -1);
if (user.startsWith("!")) {
user = user.slice(1);
}
return user;
} else {
return user;
}
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "./node_modules/gts/tsconfig-google.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "build",
"baseUrl": "./src",
"paths": {
"@lifeguard/*": ["./*"],
"@lifegaurd/base/*": ["../*"],
"@assertions/*": ["./assertions/*"],
"@config/*": ["./config/*"],
"@events/*": ["./events/*"],
"@models/*": ["./models/*"],
"@plugins/*": ["./plugins/*"],
"@structures/*": ["./structures/*"],
"@util/*": ["./util/*"]
}
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

8
tslint.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "gts/tslint.json",
"linterOptions": {
"exclude": [
"**/*.json"
]
}
}