Refactor bot. (#8)
This commit is contained in:
parent
e0374bea36
commit
259d540e6f
18 changed files with 491 additions and 300 deletions
38
src/badLinks.ts
Normal file
38
src/badLinks.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { FiltersEngine, Request } from '@cliqz/adblocker';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
let engine: FiltersEngine;
|
||||
|
||||
const init = async () => {
|
||||
if (engine) return;
|
||||
|
||||
console.log('initializing FiltersEngine');
|
||||
|
||||
engine = await FiltersEngine.fromLists(
|
||||
fetch,
|
||||
[
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt',
|
||||
'https://malware-filter.gitlab.io/malware-filter/phishing-filter.txt',
|
||||
],
|
||||
{
|
||||
enableInMemoryCache: true,
|
||||
enableOptimizations: true,
|
||||
enableCompression: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const isBad = async (url: string) => {
|
||||
await init();
|
||||
|
||||
const { match } = engine.match(
|
||||
Request.fromRawDetails({
|
||||
type: 'mainFrame',
|
||||
url,
|
||||
})
|
||||
);
|
||||
|
||||
console.log('Testing URL', url, match);
|
||||
|
||||
return match;
|
||||
};
|
32
src/commands/help.ts
Normal file
32
src/commands/help.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { MessageEmbed } from 'discord.js';
|
||||
import { commands } from '..';
|
||||
import { Command } from '..';
|
||||
|
||||
export const cmd: Command = {
|
||||
name: 'help',
|
||||
desc: 'Shows this menu.',
|
||||
exec: async (e) => {
|
||||
const embed = new MessageEmbed()
|
||||
.setTitle('Help Menu')
|
||||
.setColor('DARK_GREEN');
|
||||
let comman = commands;
|
||||
comman.sort((x, y) => {
|
||||
return x.name == 'help' ? -1 : y.name == 'help' ? 1 : 0;
|
||||
});
|
||||
for (const i in comman) {
|
||||
const cmd = comman[i];
|
||||
const resp = [];
|
||||
if (cmd.desc) {
|
||||
resp.push(cmd.desc);
|
||||
}
|
||||
if (cmd.aliases && cmd.aliases[0]) {
|
||||
resp.push(`**Aliases**: ${cmd.aliases.join(', ')}`);
|
||||
}
|
||||
if (cmd.examples && cmd.examples[0]) {
|
||||
resp.push(`**Examples**: \n${cmd.examples.join('\n> ')}`);
|
||||
}
|
||||
embed.addField('!' + cmd.name, resp.join('\n'));
|
||||
}
|
||||
return e.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
28
src/commands/members.ts
Normal file
28
src/commands/members.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Command } from '../index';
|
||||
|
||||
export const cmd: Command = {
|
||||
name: 'members',
|
||||
desc: 'Shows the amount of online users in PolyMC Discord',
|
||||
aliases: ['mems', 'memcount'],
|
||||
exec: async (e) => {
|
||||
const memes = await e.guild?.members.fetch().then((r) => r.toJSON());
|
||||
if (!memes) return;
|
||||
|
||||
return e.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: `${memes.length} total members!`,
|
||||
description: `${
|
||||
memes.filter(
|
||||
(m) =>
|
||||
m.presence?.status === 'online' ||
|
||||
m.presence?.status === 'idle' ||
|
||||
m.presence?.status === 'dnd'
|
||||
).length
|
||||
} online members`,
|
||||
color: 'GOLD',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
10
src/commands/ping.ts
Normal file
10
src/commands/ping.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Command } from '../index';
|
||||
|
||||
export const cmd: Command = {
|
||||
name: 'ping',
|
||||
desc: 'Shows the ping of the bot',
|
||||
aliases: ['test'],
|
||||
exec: async (e) => {
|
||||
return await e.reply(`${e.client.ws.ping}ms`);
|
||||
},
|
||||
};
|
20
src/commands/stars.ts
Normal file
20
src/commands/stars.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Command } from '../index';
|
||||
|
||||
export const cmd: Command = {
|
||||
name: 'stars',
|
||||
desc: 'Shows the number of stars in PolyMC',
|
||||
aliases: ['star', 'stargazers'],
|
||||
exec: async (e) => {
|
||||
const count = await fetch('https://api.github.com/repos/PolyMC/PolyMC')
|
||||
.then((r) => r.json() as Promise<{ stargazers_count: number }>)
|
||||
.then((j) => j.stargazers_count);
|
||||
return e.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: `⭐ ${count} total stars!`,
|
||||
color: 'GOLD',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
27
src/commands/tags.ts
Normal file
27
src/commands/tags.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MessageEmbed } from 'discord.js';
|
||||
import { Command } from '../index';
|
||||
import { tags } from '../index';
|
||||
|
||||
export const cmd: Command = {
|
||||
name: 'tags',
|
||||
desc: 'Lists the tags available',
|
||||
exec: async (e) => {
|
||||
const em = new MessageEmbed().setTitle('tags').setColor('DARK_GREEN');
|
||||
|
||||
for (let i in tags) {
|
||||
const tag = tags[i];
|
||||
let text = '';
|
||||
if (tag.aliases && tag.aliases[0]) {
|
||||
text += '**Aliases**: ' + tag.aliases.join(', ');
|
||||
}
|
||||
|
||||
if (tag.text) {
|
||||
text += tag.text;
|
||||
} else if (tag.embed) {
|
||||
text += '\n[embedded message]';
|
||||
}
|
||||
em.addField(tag.name, text);
|
||||
}
|
||||
return e.reply({ embeds: [em] });
|
||||
},
|
||||
};
|
3
src/constants.ts
Normal file
3
src/constants.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const GUILD_ID = '923671181020766230';
|
||||
export const DEBUG_CHANNEL_ID = '977401259260788756';
|
||||
export const POLYCAT_CHANNEL_ID = '977797790749032448';
|
34
src/filters.ts
Normal file
34
src/filters.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import * as BuildConfig from './constants';
|
||||
import { Message } from 'discord.js';
|
||||
import { isBad } from './badLinks';
|
||||
import urlRegex from 'url-regex';
|
||||
|
||||
// true if message is ok, false if filtered
|
||||
export async function filterMessage(e: Message): Promise<boolean> {
|
||||
// url matcher
|
||||
const urlMatches = [...e.content.matchAll(urlRegex())];
|
||||
|
||||
if (urlMatches.length) {
|
||||
console.log('Found links in message from', e.author.tag);
|
||||
|
||||
for (const match of urlMatches) {
|
||||
console.log('[link]', match[0]);
|
||||
if (await isBad(match[0])) {
|
||||
await e.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: 'Hold on!',
|
||||
description:
|
||||
'There seems to be a phishing / malware link in your message.',
|
||||
color: 'RED',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
157
src/index.ts
Normal file
157
src/index.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import {
|
||||
Client,
|
||||
Intents,
|
||||
Message,
|
||||
MessageEmbed,
|
||||
MessageEmbedOptions,
|
||||
} from 'discord.js';
|
||||
|
||||
import * as BuildConfig from './constants';
|
||||
import { filterMessage } from './filters';
|
||||
import { green, bold, blue, underline, yellow } from 'kleur/colors';
|
||||
import * as parser from 'discord-command-parser';
|
||||
import fs from 'fs';
|
||||
import path, { dirname } from 'path';
|
||||
import { SuccessfulParsedMessage } from 'discord-command-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import { parseLog } from './mclogs';
|
||||
dotenv.config();
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
aliases?: Array<string>;
|
||||
desc?: string;
|
||||
examples?: Array<string>;
|
||||
exec(
|
||||
m: Message,
|
||||
p: SuccessfulParsedMessage<Message<boolean>>
|
||||
): Promise<Message> | Promise<any> | any;
|
||||
}
|
||||
|
||||
type Commands = Array<Command>;
|
||||
export let commands: Commands = [];
|
||||
|
||||
interface Tag {
|
||||
name: string;
|
||||
aliases?: Array<string>;
|
||||
text?: string;
|
||||
embed?: MessageEmbedOptions;
|
||||
}
|
||||
|
||||
type Tags = Array<Tag>;
|
||||
export const tags: Tags = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'tags.json'), 'utf8')
|
||||
);
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
Intents.FLAGS.GUILDS,
|
||||
Intents.FLAGS.GUILD_MESSAGES,
|
||||
Intents.FLAGS.DIRECT_MESSAGES,
|
||||
Intents.FLAGS.GUILD_MEMBERS,
|
||||
Intents.FLAGS.GUILD_PRESENCES,
|
||||
Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
|
||||
Intents.FLAGS.GUILD_BANS,
|
||||
],
|
||||
});
|
||||
|
||||
const dir = fs.readdirSync(path.join(__dirname, '/commands'));
|
||||
for (const i in dir) {
|
||||
const cmdName = dir[i];
|
||||
const cmd: Command = require(path.join(__dirname, '/commands/', cmdName)).cmd;
|
||||
commands.push(cmd);
|
||||
}
|
||||
|
||||
client.once('ready', async () => {
|
||||
console.log(green('Discord bot ready!'));
|
||||
|
||||
if (process.env.NODE_ENV !== 'development')
|
||||
console.warn(yellow(bold('Running in production mode!')));
|
||||
|
||||
console.log(
|
||||
'Invite link:',
|
||||
blue(
|
||||
underline(
|
||||
client.generateInvite({
|
||||
scopes: ['bot'],
|
||||
permissions: ['ADMINISTRATOR'],
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const POLYMC_GUILD = await client.guilds.fetch(BuildConfig.GUILD_ID);
|
||||
|
||||
client.on('messageCreate', async (e) => {
|
||||
if (!e.content) return;
|
||||
if (!e.channel.isText()) return;
|
||||
if (e.author === client.user) return;
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
e.channelId !== BuildConfig.DEBUG_CHANNEL_ID
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
process.env.NODE_ENV !== 'development' &&
|
||||
e.channelId === BuildConfig.DEBUG_CHANNEL_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commanded = await parseMsg(e);
|
||||
if (commanded) return;
|
||||
|
||||
const log = await parseLog(e.content);
|
||||
if (log != null) {
|
||||
e.reply({ embeds: [log] });
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = await filterMessage(e);
|
||||
if (!filtered) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function parseMsg(e: Message) {
|
||||
const parsed = parser.parse(e, '!', {
|
||||
allowBots: true,
|
||||
});
|
||||
|
||||
if (!parsed.success) return false;
|
||||
const cmd = commands.find(
|
||||
(c) => c.name == parsed.command || c.aliases?.includes(parsed.command)
|
||||
);
|
||||
|
||||
if (!cmd) {
|
||||
const tag = tags.find(
|
||||
(t) => t.name == parsed.command || t.aliases?.includes(parsed.command)
|
||||
);
|
||||
if (tag) {
|
||||
if (tag.text) {
|
||||
e.reply(tag.text);
|
||||
return true;
|
||||
} else if (tag.embed) {
|
||||
const em = new MessageEmbed(tag.embed);
|
||||
e.reply({ embeds: [em] });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await cmd.exec(e, parsed);
|
||||
} catch (err: any) {
|
||||
// ts moment
|
||||
const em = new MessageEmbed()
|
||||
.setTitle('Error')
|
||||
.setColor('RED')
|
||||
.setDescription(err);
|
||||
e.reply({ embeds: [em] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
75
src/mclogs.ts
Normal file
75
src/mclogs.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { getLatest } from './version';
|
||||
import { MessageEmbed } from 'discord.js';
|
||||
const reg = /https\:\/\/mclo.gs\/[^ ]*/g;
|
||||
|
||||
type analyzer = (text: string) => Promise<Array<string> | null>;
|
||||
const javaAnalyzer: analyzer = async (text) => {
|
||||
if (text.includes('This instance is not compatible with Java version')) {
|
||||
const xp =
|
||||
/Please switch to one of the following Java versions for this instance:[\r\n]+([^\r\n]+)/g;
|
||||
|
||||
let ver: string;
|
||||
const m = text.match(xp);
|
||||
if (!m || !m[0]) {
|
||||
ver = '';
|
||||
} else {
|
||||
ver = m[0].split('\n')[1];
|
||||
}
|
||||
|
||||
return [
|
||||
'WrongJavaVersion',
|
||||
`Please switch to the following: \`${ver}\`\nFor more information, type \`!java\``,
|
||||
];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const versionAnalyzer: analyzer = async (text) => {
|
||||
const vers = text.match(/PolyMC version: [0-9].[0-9].[0-9]/g);
|
||||
if (vers && vers[0]) {
|
||||
const latest = await getLatest();
|
||||
const current = vers[0].replace('PolyMC version: ', '');
|
||||
if (latest != current) {
|
||||
return [
|
||||
'OutdatedPolyMC',
|
||||
`Your installed version is ${current}, while the newest version is ${latest}.\nPlease update, for more info see https://polymc.org/download/`,
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const analyzers: analyzer[] = [javaAnalyzer, versionAnalyzer];
|
||||
|
||||
export async function parseLog(s: string): Promise<MessageEmbed | null> {
|
||||
const r = s.match(reg);
|
||||
if (r == null || !r[0]) return null;
|
||||
const link = r[0]; // for now only first url
|
||||
const id = link.replace('https://mclo.gs/', '');
|
||||
if (!id) return null;
|
||||
const apiUrl = 'https://api.mclo.gs/1/raw/' + id;
|
||||
let log: string;
|
||||
try {
|
||||
const f = await fetch(apiUrl);
|
||||
if (f.status != 200) {
|
||||
throw 'nope';
|
||||
}
|
||||
log = await f.text();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
const embed = new MessageEmbed()
|
||||
.setTitle('Log analyzer')
|
||||
.setColor('DARK_GREEN')
|
||||
.setDescription(`Analysis of ${link} [${apiUrl}] [ID: ${id}]`);
|
||||
for (let i in analyzers) {
|
||||
const analyzer = analyzers[i];
|
||||
const out = await analyzer(log);
|
||||
if (out) embed.addField(out[0], out[1]);
|
||||
}
|
||||
if (embed.fields[0]) return embed;
|
||||
else {
|
||||
embed.addField('Analyze failed', 'No issues found automatically');
|
||||
return embed;
|
||||
}
|
||||
}
|
76
src/tags.json
Normal file
76
src/tags.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
[
|
||||
{
|
||||
"name": "migrate",
|
||||
"text": "https://polymc.org/wiki/getting-started/migrating-multimc/",
|
||||
"aliases": ["migr", "mmc"]
|
||||
},
|
||||
{
|
||||
"name": "matrix",
|
||||
"text": "https://matrix.to/#/#polymc:matrix.org"
|
||||
},
|
||||
{
|
||||
"name": "log",
|
||||
"text": "https://i.imgur.com/gsrgYzg.png"
|
||||
},
|
||||
{
|
||||
"name": "java",
|
||||
"text": "https://polymc.org/wiki/getting-started/installing-java/",
|
||||
"aliases": ["j"]
|
||||
},
|
||||
{
|
||||
"name": "paths",
|
||||
"aliases": ["dirs", "locate"],
|
||||
"embed": {
|
||||
"title": "Data directories",
|
||||
"description": "Where PolyMC stores your data (e.g. instances)",
|
||||
"color": "AQUA",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Portable (Windows / Linux)",
|
||||
"value": "In the PolyMC folder"
|
||||
},
|
||||
{
|
||||
"name": "Windows",
|
||||
"value": "`%APPDATA%/PolyMC`"
|
||||
},
|
||||
{
|
||||
"name": "macOS",
|
||||
"value": "`~/Library/Application Support/PolyMC`"
|
||||
},
|
||||
{
|
||||
"name": "Linux",
|
||||
"value": "`~/.local/share/PolyMC`"
|
||||
},
|
||||
{
|
||||
"name": "Flatpak",
|
||||
"value": "`~/.var/app/org.polymc.PolyMC/data/PolyMC`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "build",
|
||||
"text": "https://polymc.org/wiki/development/build-instructions/"
|
||||
},
|
||||
{
|
||||
"name": "eta",
|
||||
"text": "Sometime"
|
||||
},
|
||||
{
|
||||
"name": "curseforge",
|
||||
"embed": {
|
||||
"title": "What's wrong with CurseForge?",
|
||||
"description": "CurseForge added a new option to block third party clients like PolyMC from accessing mod files, and they started to enforce this option lately. We can't allow you to download those mods directly from PolyMC, but PolyMC 1.3.1 and higher have a workaround to let modpacks work: letting you to download those opted out mods manually. We highly encourage asking authors that opted out to stop doing so.",
|
||||
"color": "ORANGE"
|
||||
},
|
||||
"aliases": ["cf", "curse", "cursed"]
|
||||
},
|
||||
{
|
||||
"name": "piracy",
|
||||
"embed": {
|
||||
"title": "We don't tolerate piracy!",
|
||||
"description": "PolyMC has always been legal, legitimate & appropriate. We don't and never will have features such as offline login without an official account.",
|
||||
"color": "DARK_RED"
|
||||
}
|
||||
}
|
||||
]
|
11
src/version.ts
Normal file
11
src/version.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
let cachedVer: string;
|
||||
let cachedTimestamp: number;
|
||||
|
||||
export async function getLatest(): Promise<string> {
|
||||
if (cachedVer && Date.now() - cachedTimestamp < 600000) return cachedVer; // 10min
|
||||
const f = await fetch('https://api.github.com/repos/PolyMC/PolyMC/releases');
|
||||
const versions = await f.json();
|
||||
cachedVer = versions[0].tag_name;
|
||||
cachedTimestamp = Date.now();
|
||||
return versions[0].tag_name;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue