├── README.md
├── utils
├── filter_cache.js
├── patreon.js
├── client_settings.js
├── server_settings.js
└── invite_cache.js
├── .gitignore
├── functions
├── bot.js
├── html.js
├── moderation.js
├── levels.js
├── colours.js
├── functions.js
├── lastfm.js
├── images.js
└── discord.js
├── handlers
├── react_handler.js
├── border_handler.js
├── ready_handler.js
└── msg_handler.js
├── resources
├── css
│ ├── twittermedia.css
│ └── fmchart.css
└── JSON
│ ├── commands.json
│ ├── languages.json
│ └── help.json
├── package.json
├── tasks
├── sweep_messages.js
├── moderation.js
└── reminders.js
├── .eslintrc.json
├── db_queries
├── lastfm_db.js
├── client_db.js
├── commands_db.js
├── reminders_db.js
├── levels_db.js
├── twitter_db.js
├── server_db.js
├── vlive_db.js
├── roles_db.js
├── notifications_db.js
└── reps_db.js
├── haseul.js
└── modules
├── media.js
├── profiles.js
├── misc.js
├── patreon.js
├── levels.js
├── utility.js
├── message_logs.js
├── emojis.js
├── reminders.js
├── client.js
├── member_logs.js
├── whitelist.js
├── commands.js
├── management.js
└── information.js
/README.md:
--------------------------------------------------------------------------------
1 | # Haseul Bot
2 | A General purpose Discord bot.
3 |
--------------------------------------------------------------------------------
/utils/filter_cache.js:
--------------------------------------------------------------------------------
1 | exports.spamDomainsRegex = null;
2 | exports.deletedSpamMsgs = [];
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | archived/
2 | config*.json
3 | node_modules/
4 | haseul_data/
5 | resources/JSON/countries.json
6 | resources/icons/
7 | .vscode/
8 |
--------------------------------------------------------------------------------
/functions/bot.js:
--------------------------------------------------------------------------------
1 | const serverSettings = require('../utils/server_settings.js');
2 |
3 | exports.getPrefix = guildID => serverSettings.get(guildID, 'prefix') || '.';
4 |
--------------------------------------------------------------------------------
/handlers/react_handler.js:
--------------------------------------------------------------------------------
1 | const whitelist = require('../modules/whitelist.js');
2 |
3 | exports.onReact = async function(reaction, user) {
4 | if (user.bot) return;
5 |
6 | whitelist.onReact(reaction, user);
7 | };
8 |
--------------------------------------------------------------------------------
/functions/html.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | exports.toImage = async function(html, width, height, type='jpeg') {
4 | const image = await axios({
5 | method: 'post',
6 | url: 'http://localhost:3000/html',
7 | data: {
8 | html,
9 | width,
10 | height,
11 | imageFormat: type,
12 | quality: 100,
13 | },
14 | responseType: 'stream',
15 | });
16 |
17 | return image.data;
18 | };
19 |
--------------------------------------------------------------------------------
/utils/patreon.js:
--------------------------------------------------------------------------------
1 | const config = require('../config.json');
2 | const axios = require('axios');
3 |
4 | const tier1ID = '3938943';
5 | const tier2ID = '3950833';
6 |
7 | const patreon = axios.create({
8 | baseURL: 'https://www.patreon.com/api/oauth2/v2',
9 | timeout: 5000,
10 | headers: {
11 | 'authorization': 'Bearer ' + config.patreon_access_token,
12 | 'Content-Type': 'application/vnd.api+json',
13 | },
14 | });
15 |
16 | module.exports = {
17 | patreon,
18 | tier1ID,
19 | tier2ID,
20 | };
21 |
--------------------------------------------------------------------------------
/resources/css/twittermedia.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .media-container {
7 | display: flex;
8 | flex-direction: row;
9 | }
10 |
11 | .media-column {
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .media-image {
17 | background-size: cover;
18 | background-repeat: no-repeat;
19 | background-position: center center;
20 | background-size: cover;
21 | overflow: hidden;
22 | }
23 |
24 | .c1 {
25 | margin-right: 2px;
26 | }
27 |
28 | .c2 {
29 | margin-left: 2px;
30 | }
31 |
32 | .i1 {
33 | margin-bottom: 2px;
34 | }
35 |
36 | .i2 {
37 | margin-top: 2px;
38 | }
--------------------------------------------------------------------------------
/handlers/border_handler.js:
--------------------------------------------------------------------------------
1 | const client = require('../modules/client.js');
2 | const logs = require('../modules/member_logs.js');
3 | const roles = require('../modules/roles.js');
4 | const whitelist = require('../modules/whitelist.js');
5 | const inviteCache = require('../utils/invite_cache.js');
6 |
7 | exports.handleJoins = async function(member) {
8 | logs.join(member);
9 | roles.join(member);
10 | };
11 |
12 | exports.handleLeaves = async function(member) {
13 | logs.leave(member);
14 | };
15 |
16 | exports.handleNewGuild = async function(guild) {
17 | client.newGuild();
18 | inviteCache.newGuild(guild);
19 | whitelist.newGuild(guild);
20 | };
21 |
22 | exports.handleRemovedGuild = async function(guild) {
23 | client.removedGuild();
24 | };
25 |
--------------------------------------------------------------------------------
/resources/css/fmchart.css:
--------------------------------------------------------------------------------
1 | div {
2 | font-size: 0px;
3 | overflow: hidden;
4 | text-overflow: ellipsis;
5 | white-space: nowrap;
6 | }
7 |
8 | body {
9 | display: block;
10 | margin: 0px;
11 | }
12 |
13 | .grid {
14 | background-color: black;
15 | }
16 |
17 | .container {
18 | width: 300px;
19 | display: inline-block;
20 | position: relative;
21 | }
22 |
23 | .text {
24 | width: 296px;
25 | position: absolute;
26 | text-align: left;
27 | line-height: 1;
28 |
29 | font-family: 'Roboto Mono', 'Noto Sans CJK KR', 'Noto Sans CJK JP', 'Noto Sans CJK SC', 'Noto Sans CJK TC', monospace, sans-serif, serif;
30 | font-size: 16px;
31 | font-weight: medium;
32 | color: white;
33 | text-shadow:
34 | 1px 1px black;
35 |
36 | top: 2px;
37 | left: 2px;
38 | right:2px;
39 | }
40 |
--------------------------------------------------------------------------------
/functions/moderation.js:
--------------------------------------------------------------------------------
1 | exports.setMuteRolePerms = async function(channelsArray, muteRoleID) {
2 | if (!channelsArray || channelsArray.size < 1 || !muteRoleID) {
3 | const err = new Error('Invalid parameters given');
4 | console.error(err);
5 | } else {
6 | const permsToDeny = ['SEND_MESSAGES', 'CONNECT', 'SPEAK', 'ADD_REACTIONS'];
7 | for (const channel of channelsArray) {
8 | const mutePerms = channel.permissionsFor(muteRoleID);
9 | if (channel.viewable && mutePerms.any(permsToDeny)) {
10 | await channel.updateOverwrite(muteRoleID, {
11 | 'SEND_MESSAGES': false,
12 | 'CONNECT': false,
13 | 'SPEAK': false,
14 | 'ADD_REACTIONS': false,
15 | }, 'Updated mute role\'s permissions.').catch(console.error);
16 | }
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "haseul-bot",
3 | "version": "1.6.1",
4 | "description": "A General purpose Discord bot aimed at K-pop servers.",
5 | "main": "haseul.js",
6 | "dependencies": {
7 | "axios": "^0.21.4",
8 | "discord.js": "^13.7.0",
9 | "get-image-colors": "^2.0.1",
10 | "hashset": "0.0.6",
11 | "jsdom": "^16.7.0",
12 | "sql-template-strings": "^2.2.2",
13 | "sqlite": "^4.1.1",
14 | "sqlite3": "^5.0.8"
15 | },
16 | "scripts": {
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/twoscott/haseul-bot.git"
22 | },
23 | "author": "twoscott",
24 | "license": "ISC",
25 | "bugs": {
26 | "url": "https://github.com/twoscott/haseul-bot/issues"
27 | },
28 | "homepage": "https://github.com/twoscott/haseul-bot#readme",
29 | "devDependencies": {
30 | "eslint": "^8.13.0",
31 | "eslint-config-google": "^0.14.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tasks/sweep_messages.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('../haseul.js');
2 | const clientSettings = require('../utils/client_settings.js');
3 |
4 | exports.tasks = async function() {
5 | setInterval(async function() {
6 | const startTime = Date.now();
7 | console.log('Started sweeping old messages at ' + new Date(startTime).toUTCString());
8 |
9 | Client.sweepMessages();
10 | const whitelistChannelID = clientSettings.get('whitelistChan');
11 | if (whitelistChannelID) {
12 | console.log('Re-caching whitelist channel messages...');
13 | const whitelistChannel = await Client.channels
14 | .fetch(whitelistChannelID);
15 |
16 | // cache whitelist channel messages while still sweeping messages
17 | whitelistChannel.messages
18 | .fetch({ limit: 100 }, true);
19 | }
20 |
21 | console.log('Finished sweeping old messages, took ' + (Date.now() - startTime) / 1000 + 's');
22 | }, Client.options.messageSweepInterval * 1000);
23 | };
24 |
--------------------------------------------------------------------------------
/handlers/ready_handler.js:
--------------------------------------------------------------------------------
1 | const client = require('../modules/client.js');
2 | const whitelist = require('../modules/whitelist.js');
3 |
4 | const clientSettings = require('../utils/client_settings.js');
5 | const serverSettings = require('../utils/server_settings.js');
6 | // const inviteCache = require('../utils/invite_cache.js');
7 |
8 | const messageSweep = require('../tasks/sweep_messages.js');
9 | const moderation = require('../tasks/moderation.js');
10 | const reminders = require('../tasks/reminders.js');
11 |
12 | exports.handleTasks = async function() {
13 | console.log('Initialising modules...');
14 | const clientSettingsReady = clientSettings.onReady();
15 | const serverSettingsReady = serverSettings.onReady();
16 |
17 | Promise.all([clientSettingsReady, serverSettingsReady]).then(() => {
18 | whitelist.onReady();
19 | });
20 |
21 | client.onReady();
22 | // inviteCache.onReady();
23 |
24 | console.log('Starting tasks...');
25 | messageSweep.tasks();
26 | moderation.tasks();
27 | reminders.tasks();
28 | };
29 |
--------------------------------------------------------------------------------
/tasks/moderation.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const filterCache = require('../utils/filter_cache');
3 |
4 | const hyperphish = axios.create({
5 | baseURL: 'https://api.hyperphish.com/',
6 | timeout: 5000,
7 | });
8 |
9 | exports.tasks = async function() {
10 | updateFiltersLoop().catch(console.error);
11 | };
12 |
13 | async function updateFiltersLoop() {
14 | const startTime = Date.now();
15 |
16 | try {
17 | const res = await hyperphish.get('/gimme-domains');
18 | const spamDomains = res.data;
19 | if (!spamDomains) {
20 | return;
21 | }
22 | if (spamDomains.length < 1) {
23 | return;
24 | }
25 | filterCache.spamDomainsRegex = new RegExp(`(https?://|^|\\W)(${spamDomains.join('|')})($|\\W)`, 'i');
26 | } catch (e) {
27 | console.error(e);
28 |
29 | // 30 secs
30 | setTimeout(updateFiltersLoop, 30000 - (Date.now() - startTime));
31 | return;
32 | }
33 |
34 | setTimeout(updateFiltersLoop, 3600000 - (Date.now() - startTime)); // 1 hour
35 | }
36 |
--------------------------------------------------------------------------------
/functions/levels.js:
--------------------------------------------------------------------------------
1 | exports.guildRank = function(xp) {
2 | const lvl = Math.floor(Math.log(xp/1000+100)/Math.log(10) * 200 - 399);
3 | const baseXp = Math.ceil(((10**((lvl+399)/200))-100)*1000);
4 | const nextXp = Math.ceil(((10**((lvl+400)/200))-100)*1000);
5 | return { lvl, baseXp, nextXp };
6 | };
7 |
8 | exports.globalRank = function(xp) {
9 | const lvl = Math.floor(Math.log(xp/1000+100)/Math.log(10) * 150 - 299);
10 | const baseXp = Math.ceil(((10**((lvl+299)/150))-100)*1000);
11 | const nextXp = Math.ceil(((10**((lvl+1+300)/150))-100)*1000);
12 | return { lvl, baseXp, nextXp };
13 | };
14 |
15 | exports.cleanMsgCache = function(lastMsgCache) {
16 | const now = Date.now();
17 | console.log('Clearing message timestamp cache...');
18 | console.log('Cache size before: ' + lastMsgCache.size);
19 | for (const [userID, lastMsgTime] of lastMsgCache) {
20 | if (now - lastMsgTime > 300000 /* 5 mins*/) {
21 | lastMsgCache.delete(userID);
22 | }
23 | }
24 | console.log('Cache size after: ' + lastMsgCache.size);
25 | };
26 |
--------------------------------------------------------------------------------
/utils/client_settings.js:
--------------------------------------------------------------------------------
1 | const database = require('../db_queries/client_db.js');
2 |
3 | let settings = {};
4 |
5 | exports.template = {
6 | 'guildWhitelistChannelID': { name: 'Whitelist Channel', type: 'ID' },
7 | 'guildWhitelistOn': { name: 'Whitelist On', type: 'toggle' },
8 | };
9 |
10 | exports.onReady = async function() {
11 | settings = await database.getSettings();
12 | };
13 |
14 | exports.get = function(setting) {
15 | return settings ? settings[setting] : null;
16 | };
17 |
18 | exports.getSettings = function() {
19 | return settings;
20 | };
21 |
22 | exports.set = async function(setting, value) {
23 | await database.setVal(setting, value);
24 | if (settings) {
25 | settings[setting] = value;
26 | } else {
27 | settings = await database.getSettings();
28 | }
29 | };
30 |
31 | exports.toggle = async function(toggle) {
32 | const tog = await database.toggle(toggle);
33 | if (settings) {
34 | settings[toggle] = tog;
35 | } else {
36 | settings = await database.getSettings();
37 | }
38 | return tog;
39 | };
40 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "google"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": "latest"
12 | },
13 | "rules": {
14 | "require-jsdoc": "off",
15 | "object-curly-spacing": ["error", "always"],
16 | "indent": ["error", 4],
17 | "arrow-parens": ["error", "as-needed"],
18 | "no-unneeded-ternary": "error",
19 | "quotes": ["error", "single"],
20 | "max-len": ["error", {
21 | "code": 80,
22 | "tabWidth": 4,
23 | "ignoreUrls": true,
24 | "ignoreStrings": true,
25 | "ignoreTemplateLiterals": true,
26 | "ignoreRegExpLiterals": true,
27 | "ignoreComments": true
28 | }],
29 | "no-irregular-whitespace": ["error", {
30 | "skipStrings": true,
31 | "skipTemplates": true
32 | }],
33 | "object-shorthand": ["error", "always"],
34 | "linebreak-style": "off"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tasks/reminders.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const Client = require('../haseul.js').Client;
3 |
4 | const database = require('../db_queries/reminders_db.js');
5 |
6 | exports.tasks = async function() {
7 | remindersLoop().catch(console.error);
8 | };
9 |
10 | async function remindersLoop() {
11 | const startTime = Date.now();
12 |
13 | const overdueReminders = await database.getOverdueReminders();
14 | for (const reminder of overdueReminders) {
15 | const { reminderID, userID, remindContent, reminderSetTime } = reminder;
16 | const recipient = await Client.users.fetch(userID);
17 | if (recipient) {
18 | const embed = new Discord.MessageEmbed({
19 | title: 'Reminder!',
20 | description: remindContent,
21 | footer: { text: '📝 Reminder set' },
22 | timestamp: reminderSetTime * 1000,
23 | color: 0x01b762,
24 | });
25 | recipient.send({ content: '🔔 Reminder has been triggered.', embeds: [embed] });
26 | }
27 | database.removeReminder(reminderID);
28 | }
29 |
30 | setTimeout(remindersLoop, Math.max(10000 - (Date.now() - startTime), 0));
31 | }
32 |
--------------------------------------------------------------------------------
/db_queries/lastfm_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/lastfm.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS lfUsers(
12 | userID TEXT NOT NULL PRIMARY KEY,
13 | lfUser TEXT NOT NULL
14 | )
15 | `);
16 | });
17 |
18 | exports.setLfUser = async function(userID, lfUser) {
19 | const db = await dbopen;
20 |
21 | const statement = await db.run(SQL`
22 | INSERT INTO lfUsers VALUES (${userID}, ${lfUser})
23 | ON CONFLICT (userID) DO
24 | UPDATE SET lfUser = ${lfUser}
25 | `);
26 | return statement.changes;
27 | };
28 |
29 | exports.removeLfUser = async function(userID) {
30 | const db = await dbopen;
31 |
32 | const statement = await db.run(SQL`DELETE FROM lfUsers WHERE userID = ${userID}`);
33 | return statement.changes;
34 | };
35 |
36 |
37 | exports.getLfUser = async function(userID) {
38 | const db = await dbopen;
39 |
40 | const row = await db.get(SQL`SELECT lfUser FROM lfUsers WHERE userID = ${userID}`);
41 | return row ? row.lfUser : null;
42 | };
43 |
--------------------------------------------------------------------------------
/db_queries/client_db.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('../haseul.js');
2 |
3 | const sqlite = require('sqlite');
4 | const sqlite3 = require('sqlite3');
5 | const SQL = require('sql-template-strings');
6 | const dbopen = sqlite.open({
7 | filename: './haseul_data/client.db',
8 | driver: sqlite3.Database,
9 | });
10 |
11 | dbopen.then(db => {
12 | db.run(SQL`
13 | CREATE TABLE IF NOT EXISTS clientSettings(
14 | clientID TEXT NOT NULL PRIMARY KEY,
15 | whitelistChan TEXT,
16 | whitelistOn INT NOT NULL DEFAULT 0
17 | )
18 | `);
19 | });
20 |
21 | exports.setVal = async function(col, val) {
22 | const db = await dbopen;
23 |
24 | const statement = await db.run(`
25 | INSERT INTO clientSettings (clientID, ${col})
26 | VALUES (?, ?)
27 | ON CONFLICT (clientID) DO
28 | UPDATE SET ${col} = ?`,
29 | [Client.user.id, val, val],
30 | );
31 | return statement.changes;
32 | };
33 |
34 | exports.toggle = async function(col) {
35 | const db = await dbopen;
36 |
37 | const statement = await db.run(`
38 | UPDATE OR IGNORE clientSettings
39 | SET ${col} = ~${col} & 1
40 | WHERE clientID = ?`, [Client.user.id],
41 | );
42 |
43 | let toggle = 0;
44 | if (statement.changes) {
45 | const row = await db.get(`SELECT ${col} FROM clientSettings WHERE clientID = ?`, [Client.user.id]);
46 | toggle = row ? row[col] : 0;
47 | }
48 | return toggle;
49 | };
50 |
51 | exports.getSettings = async function() {
52 | const db = await dbopen;
53 |
54 | const row = await db.get(SQL`SELECT * FROM clientSettings WHERE clientID = ${Client.user.id}`);
55 | return row;
56 | };
57 |
--------------------------------------------------------------------------------
/functions/colours.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const getColors = require('get-image-colors');
3 |
4 | exports.randomHexColour = maxBright => {
5 | const rgb = [];
6 |
7 | let full;
8 | if (maxBright) {
9 | full = Math.floor(Math.random() * 3);
10 | rgb[full] = 'ff';
11 | }
12 |
13 | for (let i = 0; i < 3; i++) {
14 | if (i === full) continue;
15 | const val = Math.floor(Math.random() * 256).toString(16);
16 | rgb[i] = val.length === 1 ? '0'+val : val;
17 | }
18 |
19 | return rgb.join('');
20 | };
21 |
22 | exports.rgbToHex = c => {
23 | const hex = c.toString(16);
24 | return hex.length === 1 ? '0'+hex : hex;
25 | };
26 |
27 | exports.rgbToHsv = ([red, green, blue]) => {
28 | red /= 255;
29 | green /= 255;
30 | blue /= 255;
31 |
32 | const max = Math.max(red, green, blue);
33 | const min = Math.min(red, green, blue);
34 | const diff = max - min;
35 |
36 | const val = Math.round(max*100);
37 | const sat = Math.round((max == 0 ? 0 : diff / max)*100);
38 |
39 | let hue;
40 | if (max == min) {
41 | hue = 0;
42 | } else {
43 | switch (max) {
44 | case red: hue = (green - blue ) / diff + 0; break;
45 | case green: hue = (blue - red ) / diff + 2; break;
46 | case blue: hue = (red - green) / diff + 4; break;
47 | }
48 | hue /= 6;
49 | if (hue < 0) hue += 1;
50 | hue = Math.round(hue*360);
51 | }
52 |
53 | return [hue, sat, val];
54 | };
55 |
56 | exports.getImgColours = async function(url) {
57 | let imgColours = null;
58 | try {
59 | response = await axios.get(url, { responseType: 'arraybuffer' });
60 | imgColours = await getColors(response.data, response.headers['content-type']);
61 | } catch (e) {
62 | console.error(e);
63 | }
64 |
65 | return imgColours;
66 | };
67 |
68 | exports.colours = {
69 | joinColour: 0x01b762,
70 | leaveColour: 0xf93437,
71 | welcomeColour: 0x7c62d1,
72 | embedColour: 0x2f3136,
73 | };
74 |
--------------------------------------------------------------------------------
/db_queries/commands_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/commands.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(async db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS commands(
12 | guildID TEXT NOT NULL,
13 | command TEXT NOT NULL,
14 | text TEXT NOT NULL,
15 | UNIQUE(guildID, command)
16 | )
17 | `);
18 | });
19 |
20 | exports.addCommand = async function(guildID, command, text) {
21 | const db = await dbopen;
22 |
23 | const statement = await db.run(SQL`INSERT OR IGNORE INTO commands VALUES (${guildID}, ${command}, ${text})`);
24 | return statement.changes;
25 | };
26 |
27 | exports.removeCommand = async function(guildID, command) {
28 | const db = await dbopen;
29 |
30 | const statement = await db.run(SQL`DELETE FROM commands WHERE command = ${command} AND guildID = ${guildID}`);
31 | return statement.changes;
32 | };
33 |
34 | exports.renameCommand = async function(guildID, command, newCommand) {
35 | const db = await dbopen;
36 |
37 | const statement = await db.run(SQL`UPDATE OR IGNORE commands SET command = ${newCommand} WHERE command = ${command} AND guildID = ${guildID}`);
38 | return statement.changes;
39 | };
40 |
41 | exports.editCommand = async function(guildID, command, text) {
42 | const db = await dbopen;
43 |
44 | const statement = await db.run(SQL`UPDATE OR IGNORE commands SET text = ${text} WHERE command = ${command} AND guildID = ${guildID}`);
45 | return statement.changes;
46 | };
47 |
48 | exports.getCommand = async function(guildID, command) {
49 | const db = await dbopen;
50 |
51 | const row = await db.get(SQL`SELECT text FROM commands WHERE command = ${command} AND guildID = ${guildID}`);
52 | return row ? row.text : null;
53 | };
54 |
55 | exports.getCommands = async function(guildID) {
56 | const db = await dbopen;
57 |
58 | const rows = await db.all(SQL`SELECT * FROM commands WHERE guildID = ${guildID}`);
59 | return rows;
60 | };
61 |
--------------------------------------------------------------------------------
/db_queries/reminders_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/reminders.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS reminders(
12 | reminderID INTEGER PRIMARY KEY AUTOINCREMENT,
13 | userID TEXT NOT NULL,
14 | remindContent TEXT NOT NULL,
15 | remindTimestamp INT NOT NULL,
16 | reminderSetTime INT NOT NULL
17 | )
18 | `);
19 | });
20 |
21 | exports.addReminder = async function(
22 | userID, remindContent, remindTimestamp, reminderSetTime) {
23 | const db = await dbopen;
24 |
25 | const statement = await db.run(SQL`
26 | INSERT OR IGNORE INTO reminders (userID, remindContent, remindTimestamp, reminderSetTime)
27 | VALUES (${userID}, ${remindContent}, ${remindTimestamp}, ${reminderSetTime})
28 | `);
29 | return statement.lastID;
30 | };
31 |
32 | exports.removeReminder = async function(reminderID) {
33 | const db = await dbopen;
34 |
35 | const statement = await db.run(SQL`
36 | DELETE FROM reminders
37 | WHERE reminderID = ${reminderID}
38 | `);
39 | return statement.changes;
40 | };
41 |
42 | exports.clearUserReminders = async function(userID) {
43 | const db = await dbopen;
44 |
45 | const statement = await db.run(SQL`
46 | DELETE FROM reminders
47 | WHERE userID = ${userID}
48 | `);
49 | return statement.changes;
50 | };
51 |
52 | exports.getUserReminders = async function(userID) {
53 | const db = await dbopen;
54 |
55 | const reminders = await db.all(SQL`
56 | SELECT * FROM reminders
57 | WHERE userID = ${userID}
58 | `);
59 | return reminders;
60 | };
61 |
62 | exports.getOverdueReminders = async function() {
63 | const db = await dbopen;
64 |
65 | const reminders = await db.all(SQL`
66 | SELECT * FROM reminders
67 | WHERE strftime('%s', 'now') >= remindTimestamp
68 | `);
69 | return reminders;
70 | };
71 |
--------------------------------------------------------------------------------
/resources/JSON/commands.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": [
3 | "avarole",
4 | "avatar",
5 | "ban",
6 | "boosters",
7 | "cachestats",
8 | "chart",
9 | "cmd",
10 | "cmds",
11 | "colour",
12 | "color",
13 | "command",
14 | "commands",
15 | "discord",
16 | "donate",
17 | "donators",
18 | "donors",
19 | "dp",
20 | "edit",
21 | "editraw",
22 | "emoji",
23 | "emojis",
24 | "fm",
25 | "fmyt",
26 | "get",
27 | "git",
28 | "github",
29 | "guildinfo",
30 | "help",
31 | "instagram",
32 | "insta",
33 | "invite",
34 | "join",
35 | "joinLogs",
36 | "joins",
37 | "kick",
38 | "lastfm",
39 | "lb",
40 | "leaderboard",
41 | "letterboxd",
42 | "level",
43 | "levels",
44 | "lf",
45 | "lfyt",
46 | "memberinfo",
47 | "meminfo",
48 | "message",
49 | "messagelogs",
50 | "msg",
51 | "msglogs",
52 | "mute",
53 | "muterole",
54 | "noti",
55 | "notif",
56 | "notification",
57 | "notifications",
58 | "notify",
59 | "patreon",
60 | "patrons",
61 | "ping",
62 | "poll",
63 | "prefix",
64 | "profile",
65 | "purge",
66 | "remind",
67 | "reminder",
68 | "reminders",
69 | "remindme",
70 | "rank",
71 | "rep",
72 | "repboard",
73 | "reputation",
74 | "roles",
75 | "say",
76 | "sayraw",
77 | "serverboosters",
78 | "serverinfo",
79 | "settings",
80 | "sinfo",
81 | "sp",
82 | "straw",
83 | "strawpoll",
84 | "streaks",
85 | "streakboard",
86 | "supporters",
87 | "tr",
88 | "trans",
89 | "translate",
90 | "uinfo",
91 | "unban",
92 | "unmute",
93 | "userinfo",
94 | "vlive",
95 | "vpick",
96 | "whitelist",
97 | "youtube",
98 | "yt"
99 | ]
100 | }
--------------------------------------------------------------------------------
/db_queries/levels_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/levels.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(`
11 | CREATE TABLE IF NOT EXISTS globalXp(
12 | userID TEXT NOT NULL PRIMARY KEY,
13 | xp INT NOT NULL DEFAULT 0
14 | )
15 | `);
16 | db.run(`
17 | CREATE TABLE IF NOT EXISTS guildXp(
18 | userID TEXT NOT NULL,
19 | guildID TEXT NOT NULL,
20 | xp INT NOT NULL DEFAULT 0,
21 | UNIQUE(userID, guildID)
22 | )
23 | `);
24 | });
25 |
26 | exports.updateGlobalXp = async function(userID, addXp) {
27 | const db = await dbopen;
28 |
29 | const statement = await db.run(SQL`
30 | INSERT INTO globalXp VALUES (${userID}, ${addXp})
31 | ON CONFLICT (userID) DO
32 | UPDATE SET xp = xp + ${addXp}
33 | `);
34 | return statement.changes;
35 | };
36 |
37 | exports.updateGuildXp = async function(userID, guildID, addXp) {
38 | const db = await dbopen;
39 |
40 | const statement = await db.run(SQL`
41 | INSERT INTO guildXp VALUES (${userID}, ${guildID}, ${addXp})
42 | ON CONFLICT (userID, guildID) DO
43 | UPDATE SET xp = xp + ${addXp}
44 | `);
45 | return statement.changes;
46 | };
47 |
48 | exports.getGlobalXp = async function(userID) {
49 | const db = await dbopen;
50 |
51 | const row = await db.get(SQL`SELECT xp FROM globalXp WHERE userID = ${userID}`);
52 | return row ? row.xp : 0;
53 | };
54 |
55 | exports.getGuildXp = async function(userID, guildID) {
56 | const db = await dbopen;
57 |
58 | const row = await db.get(SQL`SELECT xp FROM guildXp WHERE userID = ${userID} AND guildID = ${guildID}`);
59 | return row ? row.xp : 0;
60 | };
61 |
62 | exports.getAllGlobalXp = async function() {
63 | const db = await dbopen;
64 |
65 | const rows = await db.all(SQL`SELECT * FROM globalXp`);
66 | return rows;
67 | };
68 |
69 | exports.getAllGuildXp = async function(guildID) {
70 | const db = await dbopen;
71 |
72 | const rows = await db.all(SQL`SELECT * FROM guildXp WHERE guildID = ${guildID}`);
73 | return rows;
74 | };
75 |
--------------------------------------------------------------------------------
/utils/server_settings.js:
--------------------------------------------------------------------------------
1 | const database = require('../db_queries/server_db.js');
2 |
3 | const servers = {};
4 |
5 | exports.template = {
6 | 'guildID': { name: 'Guild ID', type: 'ID' },
7 | 'prefix': { name: 'Prefix', type: 'text' },
8 | 'autoroleOn': { name: 'Autorole Toggle', type: 'toggle' },
9 | 'autoroleID': { name: 'Autorole', type: 'role' },
10 | 'commandsOn': { name: 'Commands Toggle', type: 'toggle' },
11 | 'pollOn': { name: 'Poll Toggle', type: 'toggle' },
12 | 'joinLogsOn': { name: 'Member Logs Toggle', type: 'toggle' },
13 | 'joinLogsChan': { name: 'Member Logs Channel', type: 'channel' },
14 | 'msgLogsOn': { name: 'Message Logs Toggle', type: 'toggle' },
15 | 'msgLogsChan': { name: 'Message Logs Channel', type: 'channel' },
16 | 'welcomeOn': { name: 'Welcome Toggle', type: 'toggle' },
17 | 'welcomeChan': { name: 'Welcome Channel', type: 'channel' },
18 | 'welcomeMsg': { name: 'Welcome Message', type: 'text' },
19 | 'rolesOn': { name: 'Roles Toggle', type: 'toggle' },
20 | 'rolesChannel': { name: 'Roles Channel', type: 'channel' },
21 | 'muteroleID': { name: 'Mute Role', type: 'role' },
22 | };
23 |
24 | exports.onReady = async function() {
25 | const rows = await database.getServers();
26 | for (row of rows) {
27 | servers[row.guildID] = row;
28 | }
29 | };
30 |
31 | exports.initGuild = async function(guildID) {
32 | await database.initServer(guildID);
33 | const row = await database.getServer(guildID);
34 | servers[row.guildID] = row;
35 | };
36 |
37 | exports.get = function(guildID, setting) {
38 | return servers[guildID] ? servers[guildID][setting] : null;
39 | };
40 |
41 | exports.getServer = function(guildID) {
42 | return servers[guildID];
43 | };
44 |
45 | exports.set = async function(guildID, setting, value) {
46 | await database.setVal(guildID, setting, value);
47 | if (servers[guildID]) {
48 | servers[guildID][setting] = value;
49 | } else {
50 | servers[guildID] = await database.getServer(guildID);
51 | }
52 | };
53 |
54 | exports.toggle = async function(guildID, toggle) {
55 | const tog = await database.toggle(guildID, toggle);
56 | if (servers[guildID]) {
57 | servers[guildID][toggle] = tog;
58 | } else {
59 | servers[guildID] = await database.getServer(guildID);
60 | }
61 | return tog;
62 | };
63 |
--------------------------------------------------------------------------------
/haseul.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 |
3 | const intents = new Discord.Intents();
4 | intents.add(
5 | Discord.Intents.FLAGS.GUILDS,
6 | Discord.Intents.FLAGS.GUILD_MEMBERS,
7 | Discord.Intents.FLAGS.GUILD_BANS,
8 | Discord.Intents.FLAGS.GUILD_INVITES,
9 | Discord.Intents.FLAGS.GUILD_MESSAGES,
10 | Discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
11 | Discord.Intents.FLAGS.GUILD_MESSAGE_TYPING,
12 | );
13 |
14 | const Client = new Discord.Client({
15 | disableMentions: 'everyone',
16 | messageCacheLifetime: 600,
17 | messageSweepInterval: 300,
18 | intents,
19 | });
20 | module.exports = { Client };
21 |
22 | const config = require('./config.json');
23 | const messages = require('./handlers/msg_handler.js');
24 | const reactions = require('./handlers/react_handler.js');
25 | const border = require('./handlers/border_handler.js');
26 | const checklist = require('./handlers/ready_handler.js');
27 |
28 | let initialised = false;
29 |
30 | // Debugging
31 |
32 | Client.on('shardDisconnected', closeEvent => {
33 | console.error(`Fatal error occured... Reason: ${closeEvent.reason}`);
34 | });
35 |
36 | Client.on('shardReconnecting', () => {
37 | console.log('Reconnecting...');
38 | });
39 |
40 | Client.on('error', error => {
41 | console.error(error);
42 | });
43 |
44 | Client.on('debug', debug => {
45 | console.error(debug);
46 | });
47 |
48 | Client.on('warn', warning => {
49 | console.error(warning);
50 | });
51 |
52 | // Discord
53 |
54 | Client.on('ready', async () => {
55 | console.log('Ready!');
56 |
57 | const botChannel = await Client.channels.fetch(config.bot_channel, true);
58 | if (botChannel) {
59 | botChannel.send({ content: 'Ready!' });
60 | }
61 |
62 | if (!initialised) {
63 | checklist.handleTasks();
64 | initialised = true;
65 | }
66 | });
67 |
68 | Client.on('messageCreate', message => {
69 | messages.onMessage(message);
70 | });
71 |
72 | Client.on('messageDelete', message => {
73 | messages.onMessageDelete(message);
74 | });
75 |
76 | Client.on('messageUpdate', (oldMessage, newMessage) => {
77 | messages.onMessageEdit(oldMessage, newMessage);
78 | });
79 |
80 | Client.on('messageReactionAdd', (reaction, user) => {
81 | reactions.onReact(reaction, user);
82 | });
83 |
84 | Client.on('guildMemberAdd', member => {
85 | border.handleJoins(member);
86 | });
87 |
88 | Client.on('guildMemberRemove', member => {
89 | border.handleLeaves(member);
90 | });
91 |
92 | Client.on('guildCreate', guild => {
93 | border.handleNewGuild(guild);
94 | });
95 |
96 | Client.on('guildDelete', guild => {
97 | border.handleRemovedGuild(guild);
98 | });
99 |
100 | // Login
101 |
102 | Client.login(config.token);
103 |
--------------------------------------------------------------------------------
/modules/media.js:
--------------------------------------------------------------------------------
1 | const { embedPages, withTyping } = require('../functions/discord.js');
2 | const { trimArgs } = require('../functions/functions.js');
3 |
4 | const axios = require('axios');
5 |
6 | const youtube = axios.create({
7 | baseURL: 'https://youtube.com',
8 | timeout: 5000,
9 | headers: { 'X-YouTube-Client-Name': '1', 'X-YouTube-Client-Version': '2.20200424.06.00' },
10 | });
11 |
12 | exports.onCommand = async function(message, args) {
13 | const { channel } = message;
14 |
15 | switch (args[0]) {
16 | case 'youtube':
17 | case 'yt':
18 | withTyping(channel, ytPages, [message, args]);
19 | break;
20 | }
21 | };
22 |
23 | exports.ytVidQuery = async function(query) {
24 | if (query) {
25 | const response = await youtube.get('/results', { params: { search_query: query, pbj: 1, sp: 'EgIQAQ==' } });
26 | let result;
27 | try {
28 | const data = Array.isArray(response.data) ?
29 | response.data[1] :
30 | response.data;
31 |
32 | result = data
33 | .response
34 | .contents
35 | .twoColumnSearchResultsRenderer
36 | .primaryContents
37 | .sectionListRenderer
38 | .contents[0]
39 | .itemSectionRenderer
40 | .contents[0];
41 | } catch (e) {
42 | console.error(e);
43 | return null;
44 | }
45 |
46 | return result.videoRenderer ?
47 | result.videoRenderer.videoId || null : null;
48 | }
49 | };
50 |
51 | async function ytPages(message, args) {
52 | if (args.length < 2) {
53 | message.channel.send({ content: '⚠ Please provide a query to search for!' });
54 | return;
55 | }
56 |
57 | const query = trimArgs(args, 1, message.content);
58 | const response = await youtube.get('/results', {
59 | params: { search_query: query, pbj: 1, sp: 'EgIQAQ==' },
60 | });
61 |
62 | let results;
63 | try {
64 | const data = Array.isArray(response.data) ?
65 | response.data[1] :
66 | response.data;
67 |
68 | results = data
69 | .response
70 | .contents
71 | .twoColumnSearchResultsRenderer
72 | .primaryContents
73 | .sectionListRenderer
74 | .contents[0]
75 | .itemSectionRenderer
76 | .contents;
77 | } catch (e) {
78 | console.error(e);
79 | message.channel.send({ content: '⚠ Error occurred searching YouTube.' });
80 | return;
81 | }
82 |
83 | results = results
84 | .filter(result => result.videoRenderer && result.videoRenderer.videoId)
85 | .map((result, i) => `${i + 1}. https://youtu.be/${result.videoRenderer.videoId}`);
86 |
87 | if (results.length < 1) {
88 | message.channel.send({ content: '⚠ No results found for this query!' });
89 | return;
90 | }
91 |
92 | embedPages(message, results.slice(0, 20), true);
93 | }
94 |
--------------------------------------------------------------------------------
/modules/profiles.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const { searchMembers, resolveMember, resolveUser, withTyping } = require('../functions/discord.js');
3 |
4 | const levels = require('../functions/levels.js');
5 | const { parseUserID, trimArgs } = require('../functions/functions.js');
6 |
7 | // const database = require("../db_queries/profiles_db.js");
8 | const repsdb = require('../db_queries/reps_db.js');
9 | const levelsdb = require('../db_queries/levels_db.js');
10 |
11 | exports.onCommand = async function(message, args) {
12 | const { channel } = message;
13 |
14 | switch (args[0]) {
15 | case 'profile':
16 | case 'rank':
17 | withTyping(channel, profileTemp, [message, args]);
18 | break;
19 | }
20 | };
21 |
22 | async function profileTemp(message, args) {
23 | let { author, guild, members } = message;
24 | let target = args[1];
25 | let member;
26 | let userID;
27 |
28 | if (!target) {
29 | userID = author.id;
30 | } else {
31 | userID = parseUserID(target);
32 | }
33 |
34 | if (!userID) {
35 | target = trimArgs(args, 1, message.content);
36 | members = await guild.members.fetch();
37 |
38 | member = await searchMembers(members, target);
39 | if (!member) {
40 | message.channel.send({ content: '⚠ Invalid user or user ID.' });
41 | return;
42 | } else {
43 | userID = member.id;
44 | }
45 | }
46 |
47 | const userReps = await repsdb.getRepProfile(userID);
48 | const userGlobXp = await levelsdb.getGlobalXp(userID);
49 | const userGuildXp = await levelsdb.getGuildXp(userID, guild.id);
50 |
51 | const userGlobRank = levels.globalRank(userGlobXp);
52 | const userGuildRank = levels.guildRank(userGuildXp);
53 |
54 | member = member || await resolveMember(guild, userID);
55 | if (!member) {
56 | message.channel.send({ content: '⚠ User is not in this server.' });
57 | return;
58 | }
59 | const user = member ? member.user : await resolveUser(userID);
60 | const colour = member ? member.displayColor || 0x6d5ffb : 0x6d5ffb;
61 |
62 | const embed = new Discord.MessageEmbed({
63 | author: { name: `Temp Profile for ${user.username}`, icon_url: user.displayAvatarURL({ format: 'png', dynamic: true, size: 32 }) },
64 | color: colour,
65 | fields: [
66 | { name: 'Rep', value: userReps ? userReps.rep.toString() : '0', inline: false },
67 | { name: 'Global Level', value: `Level ${userGlobXp ? userGlobRank.lvl: 1}`, inline: true },
68 | { name: 'Global XP', value: `${userGlobXp ? (userGlobXp - userGlobRank.baseXp) : 0}/${userGlobRank.nextXp - userGlobRank.baseXp}`, inline: true },
69 | { name: 'Server Level', value: `Level ${userGuildXp ? userGuildRank.lvl: 1}`, inline: true },
70 | { name: 'Server XP', value: `${userGuildXp ? (userGuildXp - userGuildRank.baseXp) : 0}/${userGuildRank.nextXp - userGuildRank.baseXp}`, inline: true },
71 | ],
72 | thumbnail: { url: user.displayAvatarURL({ format: 'png', dynamic: true, size: 512 }) },
73 | footer: { text: 'Full profiles coming soon.' },
74 | });
75 |
76 | message.channel.send({ embeds: [embed] });
77 | }
78 |
--------------------------------------------------------------------------------
/utils/invite_cache.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('../haseul.js');
2 | const { Permissions } = require('discord.js');
3 | const { checkPermissions, resolveMember } = require('../functions/discord.js');
4 |
5 | const inviteCache = new Map();
6 | const vanityCache = new Map();
7 |
8 | async function cacheGuildInvites(guild) {
9 | const botMember = await resolveMember(guild, Client.user.id);
10 | if (checkPermissions(botMember, [Permissions.FLAGS.MANAGE_GUILD])) {
11 | try {
12 | const guildInvites = await guild.invites.fetch();
13 | inviteCache.set(guild.id, guildInvites || new Map());
14 | } catch (e) {
15 | inviteCache.set(guild.id, new Map());
16 | console.error(e);
17 | }
18 | try {
19 | const vanityInvite = await guild.fetchVanityData();
20 | vanityInvite.url = `https://discord.gg/${vanityInvite.code}`;
21 | vanityCache.set(guild.id, vanityInvite);
22 | } catch (e) {
23 | vanityCache.set(guild.id, null);
24 | }
25 | } else {
26 | inviteCache.set(guild.id, new Map());
27 | vanityCache.set(guild.id, null);
28 | }
29 | }
30 |
31 | exports.newGuild = async function(guild) {
32 | cacheGuildInvites(guild);
33 | };
34 |
35 | exports.onReady = async function() {
36 | Client.guilds.cache.forEach(async guild => {
37 | await cacheGuildInvites(guild);
38 | });
39 | console.log(`Cached invites for ${inviteCache.size} servers.`);
40 | };
41 |
42 | exports.resolveUsedInvite = async function(guild) {
43 | const currentCache = await inviteCache.get(guild.id);
44 | const currentVanity = await vanityCache.get(guild.id);
45 |
46 | let usedInvite = null;
47 | let inviteChanges = 0;
48 |
49 | const newInvites = await guild.invites.fetch().catch(console.error);
50 | // console.log(newInvites);
51 | if (currentCache && newInvites && newInvites.size > 0) {
52 | for (const newInvite of newInvites.array()) {
53 | const currentInvite = currentCache.get(newInvite.code);
54 | if (currentInvite) {
55 | if (currentInvite.uses !== null &&
56 | newInvite.uses > currentInvite.uses) {
57 | usedInvite = newInvite;
58 | inviteChanges++;
59 | }
60 | } else if (newInvite.uses > 0) {
61 | usedInvite = newInvite;
62 | inviteChanges++;
63 | }
64 | }
65 | }
66 |
67 | const newVanity = await guild.fetchVanityData().catch(console.error);
68 | // console.log(newVanity);
69 | if (currentVanity && newVanity && inviteChanges < 2) {
70 | newVanity.url = `https://discord.gg/${newVanity.code}`;
71 | if (currentVanity) {
72 | if (currentVanity.uses !== null &&
73 | newVanity.uses > currentVanity.uses) {
74 | usedInvite = newVanity;
75 | inviteChanges++;
76 | }
77 | } else if (newVanity.uses > 0) {
78 | usedInvite = newVanity;
79 | inviteChanges++;
80 | }
81 | }
82 |
83 | inviteCache.set(guild.id, newInvites);
84 | vanityCache.set(guild.id, newVanity);
85 | return inviteChanges == 1 ? usedInvite : null;
86 | };
87 |
--------------------------------------------------------------------------------
/functions/functions.js:
--------------------------------------------------------------------------------
1 | exports.capitalise = text =>
2 | text[0].toUpperCase() + text.slice(1).toLowerCase();
3 |
4 | exports.trimArgs = function(args, limit, content) {
5 | for (let i = 0; i < limit; i++) {
6 | const arg = args[i];
7 | content = content.slice(content.indexOf(arg) + arg.length);
8 | }
9 | return content.trim();
10 | };
11 |
12 | exports.parseChannelID = function(text) {
13 | if (!text || text.length < 1) {
14 | const err = new Error('No text provided to resolve to channel');
15 | console.error(err);
16 | } else {
17 | const match = text.match(/#?!?(\d+)>?/);
18 | if (!match) {
19 | return null;
20 | } else {
21 | return match[1];
22 | }
23 | }
24 | };
25 |
26 | exports.parseUserID = function(text) {
27 | if (!text || text.length < 1) {
28 | const err = new Error('No text provided to resolve to channel');
29 | console.error(err);
30 | } else {
31 | const match = text.match(/@?!?(\d+)>?/);
32 | if (!match) {
33 | return null;
34 | } else {
35 | return match[1];
36 | }
37 | }
38 | };
39 |
40 | exports.getTimeAgo = (time, limit) => {
41 | const currTime = Date.now() / 1000;
42 | const timeDiffSecs = currTime - time;
43 | let timeAgoText;
44 | let timeAgo;
45 |
46 | if (timeDiffSecs < 60 || limit == 'seconds') { // 60 = minute
47 | timeAgo = Math.floor(timeDiffSecs);
48 | timeAgoText = timeAgo > 1 ? `${timeAgo} secs ago` : `${timeAgo} sec ago`;
49 | } else if (timeDiffSecs < 3600 || limit == 'minutes') { // 3600 = hour
50 | timeAgo = Math.floor((timeDiffSecs) / 60);
51 | timeAgoText = timeAgo > 1 ? `${timeAgo} mins ago` : `${timeAgo} min ago`;
52 | } else if (timeDiffSecs < 86400 || limit == 'hours') { // 86400 = day
53 | timeAgo = Math.floor((timeDiffSecs) / 3600);
54 | timeAgoText = timeAgo > 1 ? `${timeAgo} hrs ago` : `${timeAgo} hr ago`;
55 | } else if (timeDiffSecs < 604800 || limit == 'days') { // 604800 = week
56 | timeAgo = Math.floor((timeDiffSecs) / 86400);
57 | timeAgoText = timeAgo > 1 ? `${timeAgo} days ago` : `${timeAgo} day ago`;
58 | } else { // More than a week
59 | timeAgo = Math.floor((timeDiffSecs) / 604800);
60 | timeAgoText = timeAgo > 1 ? `${timeAgo} wks ago` : `${timeAgo} wk ago`;
61 | }
62 |
63 | return timeAgoText;
64 | };
65 |
66 | exports.getDelta = (ms, type) => {
67 | let delta = Math.ceil(ms / 1000);
68 | let days = 0; let hours = 0; let minutes = 0; let seconds = 0;
69 |
70 | if (['days'].includes(type) || !type) {
71 | days = Math.floor(delta / 86400);
72 | delta -= days * 86400;
73 | }
74 |
75 | if (['days', 'hours'].includes(type) || !type) {
76 | hours = Math.floor(delta / 3600);
77 | if (['days'].includes(type)) {
78 | hours = hours % 24;
79 | }
80 | delta -= hours * 3600;
81 | }
82 |
83 | if (['days', 'hours', 'minutes'].includes(type) || !type) {
84 | minutes = Math.floor(delta / 60);
85 | if (['days', 'hours'].includes(type)) {
86 | minutes = minutes % 60;
87 | }
88 | delta -= minutes * 60;
89 | }
90 |
91 | if (['days', 'hours', 'minutes', 'seconds'].includes(type) || !type) {
92 | if (['days', 'hours', 'minutes'].includes(type)) {
93 | seconds = seconds % 60;
94 | }
95 | seconds = delta % 60;
96 | }
97 |
98 | return { days, hours, minutes, seconds, ms };
99 | };
100 |
--------------------------------------------------------------------------------
/modules/misc.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const { withTyping } = require('../functions/discord.js');
3 |
4 | const html = require('../functions/html.js');
5 | const colours = require('../functions/colours.js');
6 |
7 | exports.onCommand = async function(message, args) {
8 | const { channel } = message;
9 |
10 | switch (args[0]) {
11 | case 'colour':
12 | case 'color':
13 | switch (args[1]) {
14 | case 'random':
15 | withTyping(channel, colourRandom, [message]);
16 | break;
17 | default:
18 | withTyping(channel, colour, [message, args.slice(1)]);
19 | break;
20 | }
21 | break;
22 | }
23 | };
24 |
25 | async function colour(message, args) {
26 | const colour = args.join(' ').trim();
27 | let hex = colour.match(/^(?:#|0x)([0-9a-f]{6})$/i);
28 | let rgb = colour.match(/(^\d{1,3})\s*,?\s*(\d{1,3})\s*,?\s*(\d{1,3}$)/i);
29 |
30 | if (!rgb && !hex) {
31 | message.channel.send({ content: '⚠ Please provide a valid colour hexcode or RGB values.' });
32 | return;
33 | }
34 |
35 | if (hex) {
36 | hex = hex[1];
37 | const [red, green, blue] = hex.match(/([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i).slice(1);
38 | rgb = [parseInt(red, 16), parseInt(green, 16), parseInt(blue, 16)];
39 | } else if (rgb) {
40 | rgb = rgb.slice(1).map(c => parseInt(c));
41 | const [red, green, blue] = rgb;
42 | hex = `${colours.rgbToHex(red)}${colours.rgbToHex(green)}${colours.rgbToHex(blue)}`;
43 | }
44 |
45 | const hexValue = parseInt(hex, 16);
46 | if (hexValue < 0 || hexValue > 16777215) {
47 | message.channel.send({ content: '⚠ Please provide a valid colour hexcode or RGB values.' });
48 | return;
49 | }
50 |
51 | for (const component of rgb) {
52 | if (component < 0 || component > 255) {
53 | message.channel.send({ content: '⚠ Please provide a valid colour hexcode or RGB values.' });
54 | return;
55 | }
56 | }
57 |
58 | const hsv = colours.rgbToHsv(rgb);
59 |
60 | const htmlString = `
`;
61 | const image = await html.toImage(htmlString, 200, 200);
62 |
63 | const embed = new Discord.MessageEmbed({
64 | title: `Colour \`#${hex.toLowerCase()}\``,
65 | color: hexValue,
66 | image: { url: `attachment://${hex}.jpg` },
67 | footer: { text: `RGB: ${rgb.join(', ')} | HSV: ${hsv[0]}, ${hsv[1]}%, ${hsv[2]}%` },
68 | });
69 |
70 | message.channel.send({ embed, files: [{ attachment: image, name: `${hex}.jpg` }] });
71 | }
72 |
73 | async function colourRandom(message) {
74 | const hex = '#'+colours.randomHexColour(false);
75 | const [red, green, blue] = hex.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i).slice(1);
76 |
77 | const rgb = [parseInt(red, 16), parseInt(green, 16), parseInt(blue, 16)];
78 | const hsv = colours.rgbToHsv(rgb);
79 |
80 | const htmlString = ` `;
81 | const image = await html.toImage(htmlString, 200, 200);
82 |
83 | const embed = new Discord.MessageEmbed({
84 | title: `Colour \`${hex.toLowerCase()}\``,
85 | color: parseInt(hex.split('#')[1], 16),
86 | image: { url: `attachment://${hex.replace('#', '')}.jpg` },
87 | footer: { text: `RGB: ${rgb.join(', ')} | HSV: ${hsv[0]}, ${hsv[1]}%, ${hsv[2]}%` },
88 | });
89 |
90 | message.channel.send({ embed, files: [{ attachment: image, name: `${hex.replace('#', '')}.jpg` }] });
91 | }
92 |
--------------------------------------------------------------------------------
/db_queries/twitter_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/twitter.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS twitterChannels(
12 | guildID TEXT NOT NULL,
13 | channelID TEXT NOT NULL,
14 | twitterID TEXT NOT NULL,
15 | screenName TEXT NOT NUll,
16 | mentionRoleID TEXT,
17 | retweets DEFAULT 1,
18 | UNIQUE(channelID, twitterID)
19 | )
20 | `);
21 | db.run(SQL`
22 | CREATE TABLE IF NOT EXISTS tweets(
23 | twitterID TEXT NOT NULL,
24 | tweetID TEXT NOT NULL PRIMARY KEY
25 | )
26 | `);
27 | });
28 |
29 | exports.addTwitterChannel = async function(
30 | guildID, channelID, twitterID, screenName, mentionRole) {
31 | const db = await dbopen;
32 |
33 | const statement = await db.run(SQL`
34 | INSERT OR IGNORE
35 | INTO twitterChannels (guildID, channelID, twitterID, screenName, mentionRoleID)
36 | VALUES (${guildID}, ${channelID}, ${twitterID}, ${screenName}, ${mentionRole})
37 | `);
38 | return statement.changes;
39 | };
40 |
41 | exports.removeTwitterChannel = async function(channelID, twitterID) {
42 | const db = await dbopen;
43 |
44 | const statement = await db.run(SQL`
45 | DELETE FROM twitterChannels
46 | WHERE channelID = ${channelID} AND twitterID = ${twitterID}
47 | `);
48 | return statement.changes;
49 | };
50 |
51 | exports.getTwitterChannel = async function(channelID, twitterID) {
52 | const db = await dbopen;
53 |
54 | const row = await db.get(SQL`
55 | SELECT * FROM twitterChannels
56 | WHERE channelID = ${channelID} AND twitterID = ${twitterID}
57 | `);
58 | return row;
59 | };
60 |
61 | exports.getGuildTwitterChannels = async function(guildID) {
62 | const db = await dbopen;
63 |
64 | const rows = await db.all(SQL`SELECT * FROM twitterChannels WHERE guildID = ${guildID}`);
65 | return rows;
66 | };
67 |
68 | exports.getAllTwitterChannels = async function() {
69 | const db = await dbopen;
70 |
71 | const rows = await db.all(SQL`SELECT * FROM twitterChannels`);
72 | return rows;
73 | };
74 |
75 | exports.toggleRetweets = async function(channelID, twitterID) {
76 | const db = await dbopen;
77 |
78 | const statement = await db.run(SQL`
79 | UPDATE OR IGNORE twitterChannels
80 | SET retweets = ~retweets & 1
81 | WHERE channelID = ${channelID} AND twitterID = ${twitterID}
82 | `);
83 |
84 | let toggle = 0;
85 | if (statement.changes) {
86 | const row = await db.get(SQL`SELECT retweets FROM twitterChannels WHERE channelID = ${channelID} AND twitterID = ${twitterID}`);
87 | toggle = row ? row.retweets : 0;
88 | }
89 | return toggle;
90 | };
91 |
92 | exports.addTweet = async function(twitterID, tweetID) {
93 | const db = await dbopen;
94 |
95 | const statement = await db.run(SQL`INSERT OR IGNORE INTO tweets VALUES (${twitterID}, ${tweetID})`);
96 | return statement.changes;
97 | };
98 |
99 | exports.getAccountTweets = async function(twitterID) {
100 | const db = await dbopen;
101 |
102 | const rows = await db.all(SQL`SELECT * FROM tweets WHERE twitterID = ${twitterID}`);
103 | return rows;
104 | };
105 |
106 | exports.getAllTweets = async function() {
107 | const db = await dbopen;
108 |
109 | const rows = await db.all(SQL`SELECT * FROM tweets`);
110 | return rows;
111 | };
112 |
--------------------------------------------------------------------------------
/db_queries/server_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/servers.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS pollChans(
12 | guildID TEXT NOT NULL,
13 | channelID TEXT NOT NULL,
14 | UNIQUE(guildID, channelID)
15 | )
16 | `);
17 | db.run(SQL`
18 | CREATE TABLE IF NOT EXISTS serverSettings(
19 | guildID TEXT NOT NULL PRIMARY KEY,
20 | prefix TEXT NOT NULL DEFAULT '.',
21 | autoroleOn INT NOT NULL DEFAULT 0,
22 | autoroleID TEXT,
23 | commandsOn INT NOT NULL DEFAULT 1,
24 | pollOn INT NOT NULL DEFAULT 0,
25 | joinLogsOn INT NOT NULL DEFAULT 0,
26 | joinLogsChan TEXT,
27 | msgLogsOn INT NOT NULL DEFAULT 0,
28 | msgLogsChan TEXT,
29 | welcomeOn INT NOT NULL DEFAULT 0,
30 | welcomeChan TEXT,
31 | welcomeMsg TEXT,
32 | rolesOn INT NOT NULL DEFAULT 0,
33 | rolesChannel TEXT,
34 | muteroleID TEXT
35 | )
36 | `);
37 | });
38 |
39 | // dbopen.then(async db => {
40 | // await db.run(SQL`ALTER TABLE serverSettings ADD COLUMN spamFilterLvl INT NOT NULL DEFAULT 0`);
41 | // console.log("Finished altering servers.db");
42 | // })
43 |
44 | exports.setVal = async function(guildID, col, val) {
45 | const db = await dbopen;
46 |
47 | const statement = await db.run(`
48 | UPDATE OR IGNORE serverSettings
49 | SET ${col} = ?
50 | WHERE guildID = ?`,
51 | [val, guildID],
52 | );
53 | return statement.changes;
54 | };
55 |
56 | exports.toggle = async function(guildID, col) {
57 | const db = await dbopen;
58 |
59 | const statement = await db.run(`
60 | UPDATE OR IGNORE serverSettings
61 | SET ${col} = ~${col} & 1
62 | WHERE guildID = ?`, [guildID],
63 | );
64 |
65 | let toggle = 0;
66 | if (statement.changes) {
67 | const row = await db.get(`SELECT ${col} FROM serverSettings WHERE guildID = ?`, [guildID]);
68 | toggle = row ? row[col] : 0;
69 | }
70 | return toggle;
71 | };
72 |
73 | exports.initServer = async function(guildID) {
74 | const db = await dbopen;
75 |
76 | const statement = await db.run(SQL`
77 | INSERT OR IGNORE INTO serverSettings
78 | (guildID)
79 | VALUES (${guildID})
80 | `);
81 | return statement.changes;
82 | };
83 |
84 | exports.getServers = async function() {
85 | const db = await dbopen;
86 |
87 | const rows = await db.all(SQL`SELECT * FROM serverSettings`);
88 | return rows;
89 | };
90 |
91 | exports.getServer = async function(guildID) {
92 | const db = await dbopen;
93 |
94 | const row = await db.get(SQL`SELECT * FROM serverSettings WHERE guildID = ${guildID}`);
95 | return row;
96 | };
97 |
98 | exports.addPollChannel = async function(guildID, channelID) {
99 | const db = await dbopen;
100 |
101 | const statement = await db.run(SQL`INSERT OR IGNORE INTO pollChans VALUES (${guildID}, ${channelID})`);
102 | return statement.changes;
103 | };
104 |
105 | exports.removePollChannel = async function(guildID, channelID) {
106 | const db = await dbopen;
107 |
108 | const statement = await db.run(SQL`DELETE FROM pollChans WHERE guildID = ${guildID} AND channelID = ${channelID}`);
109 | return statement.changes;
110 | };
111 |
112 | exports.getPollChannels = async function(guildID) {
113 | const db = await dbopen;
114 |
115 | const rows = await db.all(SQL`SELECT channelID FROM pollChans WHERE guildID = ${guildID}`);
116 | return rows.map(x => x.channelID);
117 | };
118 |
--------------------------------------------------------------------------------
/modules/patreon.js:
--------------------------------------------------------------------------------
1 | const { embedPages, withTyping } = require('../functions/discord.js');
2 | const { Client } = require('../haseul.js');
3 |
4 | const config = require('../config.json');
5 | const { patreon } = require('../utils/patreon.js');
6 |
7 | exports.onCommand = async function(message, args) {
8 | const { channel } = message;
9 |
10 | switch (args[0]) {
11 | case 'donate':
12 | case 'patreon':
13 | message.channel.send({ content: 'https://www.patreon.com/haseulbot' });
14 | break;
15 | case 'donors':
16 | case 'donators':
17 | case 'patrons':
18 | case 'supporters':
19 | withTyping(channel, patrons, [message]);
20 | break;
21 | }
22 | };
23 |
24 | async function patrons(message) {
25 | let response;
26 | try {
27 | response = await patreon.get('/campaigns/'+config.haseul_campaign_id+'/members?include=user&fields'+encodeURI('[member]')+'=full_name,patron_status,pledge_relationship_start&fields'+encodeURI('[user]')+'=social_connections');
28 | } catch (e) {
29 | console.error('Patreon error: ' + e.response.status);
30 | message.channel.send({ content: '⚠ Error occurred.' });
31 | return;
32 | }
33 |
34 | try {
35 | const members = response.data.data.filter(m => m.attributes.patron_status == 'active_patron');
36 | const users = response.data.included.filter(x => x.type == 'user');
37 | if (members.length < 1) {
38 | message.channel.send({ content: 'Nobody is currently supporting Haseul Bot :pensive:' });
39 | return;
40 | }
41 |
42 | memberString = members.sort((a, b) => {
43 | const aPledgeTime = new Date(a.attributes.pledge_relationship_start)
44 | .getTime();
45 | const bPledgeTime = new Date(b.attributes.pledge_relationship_start)
46 | .getTime();
47 | return aPledgeTime - bPledgeTime;
48 | }).filter(member => {
49 | const user = users
50 | .find(u => u.id == member.relationships.user.data.id);
51 | const socials = user.attributes.social_connections;
52 | return socials.discord;
53 | }).map(member => {
54 | const user = users
55 | .find(u => u.id == member.relationships.user.data.id);
56 | const { discord } = user.attributes.social_connections;
57 | const discordUser = Client.users.cache.get(discord.user_id);
58 | return `<@${discord.user_id}> (${discordUser ? discordUser.tag : discord.user_id})`;
59 | }).join('\n');
60 |
61 | const descriptions = [];
62 | while (memberString.length > 2048 || memberString.split('\n').length > 25) {
63 | let currString = memberString.slice(0, 2048);
64 |
65 | let lastIndex = 0;
66 | for (let i = 0; i < 25; i++) {
67 | const index = currString.indexOf('\n', lastIndex) + 1;
68 | if (index) lastIndex = index; else break;
69 | }
70 | currString = currString.slice(0, lastIndex);
71 | memberString = memberString.slice(lastIndex);
72 |
73 | descriptions.push(currString);
74 | }
75 | descriptions.push(memberString);
76 |
77 | const pages = descriptions.map((desc, i) => ({
78 | embeds: [{
79 | author: {
80 | name: 'Haseul Bot Patrons', icon_url: 'https://i.imgur.com/iUKnebH.png',
81 | },
82 | description: desc,
83 | color: 0xf2abba,
84 | footer: {
85 | text: `Thank you for supporting me! ${descriptions.length > 1 ? `| Page ${i+1} of ${descriptions.length}`:''}`,
86 | },
87 | }],
88 | }));
89 |
90 | embedPages(message, pages);
91 | } catch (e) {
92 | message.channel.send({ content: '⚠ Unknown error occurred.' });
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/modules/levels.js:
--------------------------------------------------------------------------------
1 | const { embedPages, resolveUser, withTyping } = require('../functions/discord.js');
2 |
3 | const database = require('../db_queries/levels_db.js');
4 | const { globalRank, guildRank, cleanMsgCache } = require('../functions/levels.js');
5 |
6 | const lastMsgCache = new Map();
7 | setInterval(cleanMsgCache, 300000, lastMsgCache);
8 |
9 | exports.onMessage = async function(message) {
10 | updateUserXp(message);
11 | };
12 |
13 | exports.onCommand = async function(message, args) {
14 | const { channel } = message;
15 |
16 | switch (args[0]) {
17 | case 'leaderboard':
18 | switch (args[1]) {
19 | case 'global':
20 | withTyping(channel, leaderboard, [message, false]);
21 | break;
22 | case 'local':
23 | default:
24 | withTyping(channel, leaderboard, [message, true]);
25 | break;
26 | }
27 | break;
28 | }
29 | };
30 |
31 | function updateUserXp(message) {
32 | const { author, guild, createdTimestamp } = message;
33 |
34 | let addXp = 5;
35 | const wordcount = message.content.split(/\s+/).length;
36 | if (wordcount > 12) {
37 | addXp += 10;
38 | } else if (wordcount > 3) {
39 | addXp += 5;
40 | }
41 | if (message.attachments.size > 0) {
42 | addXp += 10;
43 | }
44 |
45 | if (createdTimestamp - lastMsgCache
46 | .get(author.id) < 15000 /* 15 seconds*/) {
47 | addXp = Math.floor(addXp/5);
48 | }
49 |
50 | lastMsgCache.set(author.id, createdTimestamp);
51 | database.updateGlobalXp(author.id, addXp);
52 | database.updateGuildXp(author.id, guild.id, addXp);
53 | }
54 |
55 | async function leaderboard(message, local) {
56 | const { guild } = message;
57 | let ranks = local ? await database.getAllGuildXp(guild.id) :
58 | await database.getAllGlobalXp();
59 |
60 | if (ranks.length < 1) {
61 | message.channel.send({ content: `⚠ Nobody${local ? ' on this server ':' '}currently has any xp!` });
62 | return;
63 | }
64 |
65 | const entries = ranks.length;
66 | const originalRanks = ranks.slice();
67 | ranks = ranks.sort((a, b) => b.xp - a.xp)
68 | .slice(0, 100); // show only top 100
69 | for (let i = 0; i < ranks.length; i++) {
70 | const rank = ranks[i];
71 | const user = await resolveUser(rank.userID);
72 | const name = user ? user.username.replace(/([\`\*\~\_])/g, '\\$&') : rank.userID;
73 | ranks[i] = {
74 | userID: rank.userID, name,
75 | lvl: local ?
76 | guildRank(rank.xp).lvl :
77 | globalRank(rank.xp).lvl, xp: rank.xp,
78 | };
79 | }
80 |
81 | let rankString = ranks.map((data, i) => `${i+1}. **${data.name.replace(/([\(\)\`\*\~\_])/g, '\\$&')}** (Lvl ${data.lvl} - ${data.xp.toLocaleString()} XP)`).join('\n');
82 |
83 | const descriptions = [];
84 | while (rankString.length > 2048 || rankString.split('\n').length > 25) {
85 | let currString = rankString.slice(0, 2048);
86 |
87 | let lastIndex = 0;
88 | for (let i = 0; i < 25; i++) {
89 | const index = currString.indexOf('\n', lastIndex) + 1;
90 | if (index) lastIndex = index; else break;
91 | }
92 | currString = currString.slice(0, lastIndex);
93 | rankString = rankString.slice(lastIndex);
94 |
95 | descriptions.push(currString);
96 | }
97 | descriptions.push(rankString);
98 |
99 | const pages = descriptions.map((desc, i) => ({
100 | embeds: [{
101 | author: {
102 | name: `${local ? guild.name : 'Global'} Leaderboard`, icon_url: 'https://i.imgur.com/qfUfBps.png',
103 | },
104 | description: desc,
105 | color: 0x6d5ffb,
106 | footer: {
107 | text: `Entries: ${entries} | Avg. Lvl: ${Math.round(originalRanks.reduce((acc, curr) => acc + local ? guildRank(curr.xp).lvl : globalRank(curr.xp).lvl, 0) / originalRanks.length)} | Page ${i+1} of ${descriptions.length}`,
108 | },
109 | }],
110 | }));
111 |
112 | embedPages(message, pages);
113 | }
114 |
--------------------------------------------------------------------------------
/functions/lastfm.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const { JSDOM } = require('jsdom');
3 |
4 | exports.scrapeArtistImage = async function(artist) {
5 | let response;
6 | try {
7 | response = await axios.get(`https://www.last.fm/music/${encodeURIComponent(artist)}/+images`);
8 | } catch (e) {
9 | const err = new Error(e.response.data);
10 | console.error(err);
11 | return null;
12 | }
13 |
14 | const doc = new JSDOM(response.data).window.document;
15 | const images = doc.getElementsByClassName('image-list-item-wrapper');
16 | if (images.length < 1) {
17 | return 'https://lastfm-img2.akamaized.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png';
18 | }
19 |
20 | return images[0].getElementsByTagName('img')[0].src.replace('/avatar170s/', '/300x300/') + '.png';
21 | };
22 |
23 | exports.scrapeArtistsWithImages = async function(
24 | username, datePreset, itemCount) {
25 | const collection = [];
26 | const pageCount = Math.ceil(itemCount / 50);
27 |
28 | for (let i = 0; i < pageCount; i++) {
29 | if (i > 0 && collection.length < 50) break;
30 | let response;
31 | try {
32 | response = await axios.get(`https://www.last.fm/user/${username}/library/artists?date_preset=${datePreset}&page=${i+1}`);
33 | } catch (e) {
34 | return null;
35 | }
36 |
37 | const doc = new JSDOM(response.data).window.document;
38 | const rows = doc.getElementsByClassName('chartlist-row link-block-basic js-link-block');
39 | for (let j = 0; j < rows.length && j < itemCount - (i*50); j++) {
40 | const row = rows[j];
41 | collection.push({
42 | 'image': [{
43 | '#text': row.getElementsByClassName('avatar')[0].getElementsByTagName('img')[0].src.replace('/avatar70s/', '/300x300/'),
44 | }],
45 | 'name': row.getElementsByClassName('link-block-target')[0].textContent,
46 | 'playcount': row.getElementsByClassName('chartlist-count-bar-value')[0].textContent.match(/[0-9,]+/)[0],
47 | });
48 | }
49 | }
50 |
51 | return collection;
52 | };
53 |
54 | exports.getTimeFrame = function(timeframe) {
55 | let displayTime;
56 | let datePreset;
57 | let defaulted = false;
58 |
59 | const week = ['7', '7day', '7days', 'weekly', 'week', '1week'];
60 | const month = ['30', '30day', '30days', 'monthly', 'month', '1month'];
61 | const threeMonth = ['90', '90day', '90days', '3months', '3month'];
62 | const sixMonth = ['180', '180day', '180days', '6months', '6month'];
63 | const year = ['365', '365day', '365days', '1year', 'year', 'yr', '12months', '12month', 'yearly'];
64 | const overall = ['all', 'at', 'alltime', 'forever', 'overall'];
65 |
66 | switch (true) {
67 | case week.includes(timeframe):
68 | timeframe = '7day';
69 | displayTime = 'Last Week';
70 | datePreset = 'LAST_7_DAYS';
71 | break;
72 | case month.includes(timeframe):
73 | timeframe = '1month';
74 | displayTime = 'Last Month';
75 | datePreset = 'LAST_30_DAYS';
76 | break;
77 | case threeMonth.includes(timeframe):
78 | timeframe = '3month';
79 | displayTime = 'Last 3 Months';
80 | datePreset = 'LAST_90_DAYS';
81 | break;
82 | case sixMonth.includes(timeframe):
83 | timeframe = '6month';
84 | displayTime = 'Last 6 Months';
85 | datePreset = 'LAST_180_DAYS';
86 | break;
87 | case year.includes(timeframe):
88 | timeframe = '12month';
89 | displayTime = 'Last Year';
90 | datePreset = 'LAST_365_DAYS';
91 | break;
92 | case overall.includes(timeframe):
93 | timeframe = 'overall';
94 | displayTime = 'All Time';
95 | datePreset = 'ALL';
96 | break;
97 | default:
98 | timeframe = '7day';
99 | displayTime = 'Last Week';
100 | datePreset = 'LAST_7_DAYS';
101 | defaulted = true;
102 | }
103 |
104 | return {
105 | timeframe,
106 | displayTime,
107 | datePreset,
108 | defaulted,
109 | };
110 | };
111 |
--------------------------------------------------------------------------------
/modules/utility.js:
--------------------------------------------------------------------------------
1 | const { withTyping } = require('../functions/discord.js');
2 |
3 | const axios = require('axios');
4 |
5 | const config = require('../config.json');
6 | const langs = require('../resources/JSON/languages.json');
7 | const { trimArgs } = require('../functions/functions.js');
8 |
9 | const translateKey = config.trans_key;
10 |
11 | exports.onCommand = async function(message, args) {
12 | const { channel } = message;
13 |
14 | switch (args[0]) {
15 | case 'github':
16 | case 'git':
17 | message.channel.send({ content: 'https://github.com/twoscott/haseul-bot' });
18 | break;
19 | case 'discord':
20 | case 'invite':
21 | message.channel.send({ content: 'https://discord.gg/w4q5qux' });
22 | break;
23 | case 'translate':
24 | case 'trans':
25 | case 'tr':
26 | withTyping(channel, translate, [message, args]);
27 | break;
28 | case 'help':
29 | message.channel.send({ content: 'Commands can be found here: https://haseulbot.xyz/' });
30 | break;
31 | case 'ping':
32 | const start = Date.now();
33 | message.channel.send({ content: 'Response: ' }).then(msg => {
34 | const end = Date.now();
35 | const ms = end - start;
36 | msg.edit(`Response: \`${ms}ms\``);
37 | });
38 | break;
39 | }
40 | };
41 |
42 | async function translate(message, args) {
43 | if (args.length < 2) {
44 | return 'Help with translation can be found here: https://haseulbot.xyz/#misc';
45 | }
46 |
47 | const langOptions = args[1];
48 | const text = trimArgs(args, 2, message.content);
49 |
50 | if (langOptions.toLowerCase() == 'languages') {
51 | return 'Language codes can be found here: https://haseulbot.xyz/languages/';
52 | }
53 |
54 | let sourceLang;
55 | let targetLang;
56 | const langOptionsArray = langOptions.split('-');
57 | if (langOptionsArray.length > 2) {
58 | if (langOptions.toLowerCase().startsWith('zh-hans') || langOptions.toLowerCase().startsWith('zh-hant')) {
59 | sourceLang = langOptionsArray.slice(0, 2).join('-');
60 | targetLang = langOptionsArray[2];
61 | } else {
62 | sourceLang = langOptionsArray[0];
63 | targetLang = langOptionsArray.slice(1).join('-');
64 | }
65 | } else if (langOptionsArray.length > 1) {
66 | sourceLang = langOptionsArray[0];
67 | targetLang = langOptionsArray[1];
68 | } else {
69 | targetLang = langOptions;
70 | }
71 |
72 | let sourceLangCode;
73 | if (sourceLang) {
74 | if (langs[sourceLang]) {
75 | sourceLangCode = sourceLang;
76 | } else {
77 | for (property in langs) {
78 | if (langs[property].name.toLowerCase() ==
79 | sourceLang.toLowerCase()) {
80 | sourceLangCode = property;
81 | }
82 | }
83 | }
84 | }
85 | let targetLangCode;
86 | if (langs[targetLang]) {
87 | targetLangCode = targetLang;
88 | } else {
89 | for (property in langs) {
90 | if (langs[property].name.toLowerCase() ==
91 | targetLang.toLowerCase()) {
92 | targetLangCode = property;
93 | }
94 | }
95 | }
96 |
97 | if (!targetLangCode) {
98 | message.channel.send({ content: '⚠ Invalid language or language code given.' });
99 | return;
100 | }
101 | if (!text) {
102 | message.channel.send({ content: '⚠ No text given to be translated.' });
103 | return;
104 | }
105 |
106 | let url = `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${encodeURIComponent(targetLangCode)}`;
107 | if (sourceLangCode) {
108 | url += `&from=${encodeURIComponent(sourceLangCode)}`;
109 | }
110 |
111 | const translation = await axios.post(url, [{ 'Text': text }], {
112 | headers: { 'Ocp-Apim-Subscription-Key': translateKey },
113 | });
114 | const { detectedLanguage, translations } = translation.data[0];
115 | const sourceLanguage = detectedLanguage ?
116 | detectedLanguage.language :
117 | sourceLangCode;
118 | const targetLanguage = translations[0].to;
119 |
120 | message.channel.send({ content: `**${sourceLanguage}-${targetLanguage}** Translation: ${translations[0].text}` });
121 | }
122 |
--------------------------------------------------------------------------------
/handlers/msg_handler.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const { resolveMember } = require('../functions/discord.js');
3 | const { Client } = require('../haseul.js');
4 | const { getPrefix } = require('../functions/bot.js');
5 |
6 | const client = require('../modules/client.js');
7 | const commands = require('../modules/commands.js');
8 | const emojis = require('../modules/emojis.js');
9 | const information = require('../modules/information.js');
10 | const lastfm = require('../modules/lastfm.js');
11 | const levels = require('../modules/levels.js');
12 | const management = require('../modules/management.js');
13 | const media = require('../modules/media.js');
14 | const memberLogs = require('../modules/member_logs.js');
15 | const messageLogs = require('../modules/message_logs.js');
16 | const misc = require('../modules/misc.js');
17 | const moderation = require('../modules/moderation.js');
18 | const notifications = require('../modules/notifications.js');
19 | const patreon = require('../modules/patreon.js');
20 | const profiles = require('../modules/profiles.js');
21 | const reminders = require('../modules/reminders.js');
22 | const reps = require('../modules/reps.js');
23 | const roles = require('../modules/roles.js');
24 | const utility = require('../modules/utility.js');
25 | const whitelist = require('../modules/whitelist.js');
26 |
27 | exports.onMessage = async function(message) {
28 | if (message.system) return;
29 | if (message.author.bot) return;
30 | if (message.channel.type === Discord.Constants.ChannelTypes.DM) return;
31 |
32 | const { author, content, guild } = message;
33 | const prefix = getPrefix(guild.id);
34 |
35 | if (content.startsWith(prefix)) {
36 | const args = content.slice(1).split(/\s+/);
37 | if (!message.member) {
38 | message.member = await resolveMember(guild, author.id);
39 | }
40 | processCommand(message, args);
41 | }
42 |
43 | if (message.mentions.users.has(Client.user.id) && !message.reference) {
44 | const args = content.split(/\s+/);
45 | if (!message.member) {
46 | message.member = await resolveMember(guild, author.id);
47 | }
48 | processMention(message, args);
49 | }
50 |
51 | processMessage(message);
52 | };
53 |
54 | exports.onMessageDelete = async function(message) {
55 | if (message.system) return;
56 | if (message.author.bot) return;
57 | if (message.channel.type === Discord.Constants.ChannelTypes.DM) return;
58 | message.deletedTimestamp = Date.now();
59 | message.deletedAt = new Date(message.deletedTimestamp);
60 |
61 | processMessageDelete(message);
62 | };
63 |
64 | exports.onMessageEdit = async function(oldMessage, newMessage) {
65 | if (oldMessage.system || newMessage.system) return;
66 | if (oldMessage.author.bot || newMessage.author.bot) return;
67 | if (oldMessage.channel.type === Discord.Constants.ChannelTypes.DM ||
68 | newMessage.channel.type === Discord.Constants.ChannelTypes.DM) {
69 | return;
70 | }
71 |
72 | processMessageEdit(oldMessage, newMessage);
73 | };
74 |
75 | async function processCommand(message, args) {
76 | client.onCommand(message, args);
77 | commands.onCommand(message, args);
78 | emojis.onCommand(message, args);
79 | information.onCommand(message, args);
80 | lastfm.onCommand(message, args);
81 | levels.onCommand(message, args);
82 | management.onCommand(message, args);
83 | media.onCommand(message, args);
84 | memberLogs.onCommand(message, args);
85 | messageLogs.onCommand(message, args);
86 | misc.onCommand(message, args);
87 | moderation.onCommand(message, args);
88 | notifications.onCommand(message, args);
89 | patreon.onCommand(message, args);
90 | profiles.onCommand(message, args);
91 | reminders.onCommand(message, args);
92 | reps.onCommand(message, args);
93 | roles.onCommand(message, args);
94 | utility.onCommand(message, args);
95 | whitelist.onCommand(message, args);
96 | }
97 |
98 | async function processMention(message, args) {
99 | emojis.onMention(message, args);
100 | client.onMention(message, args);
101 | }
102 |
103 | async function processMessage(message) {
104 | levels.onMessage(message);
105 | management.onMessage(message);
106 | moderation.onMessage(message);
107 | notifications.onMessage(message);
108 | roles.onMessage(message);
109 | whitelist.onMessage(message);
110 | }
111 |
112 | async function processMessageDelete(message) {
113 | messageLogs.onMessageDelete(message);
114 | }
115 |
116 | async function processMessageEdit(oldMessage, newMessage) {
117 | messageLogs.onMessageEdit(oldMessage, newMessage);
118 | }
119 |
--------------------------------------------------------------------------------
/db_queries/vlive_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/vlive.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS channelArchive(
12 | channelCode TEXT NOT NULL PRIMARY KEY,
13 | channelName TEXT NOT NULL,
14 | channelPlusType TEXT
15 | )
16 | `);
17 | db.run(SQL`
18 | CREATE TABLE IF NOT EXISTS vliveChannels(
19 | guildID TEXT NOT NULL,
20 | discordChanID TEXT NOT NULL,
21 | channelSeq INT NOT NULL,
22 | channelCode TEXT NOT NULL,
23 | channelName TEXT NOT NULL,
24 | mentionRoleID TEXT,
25 | VPICK INT NOT NULL DEFAULT 1,
26 | UNIQUE(discordChanID, ChannelSeq)
27 | )
28 | `);
29 | db.run(SQL`
30 | CREATE TABLE IF NOT EXISTS vliveVideos(
31 | videoSeq INT NOT NULL,
32 | channelSeq INT NOT NULL,
33 | UNIQUE(videoSeq, ChannelSeq)
34 | )
35 | `);
36 | });
37 |
38 | exports.updateArchiveChannel = async function(
39 | channelCode, channelName, channelPlusType) {
40 | const db = await dbopen;
41 |
42 | const statement = await db.run(SQL`
43 | INSERT INTO channelArchive
44 | VALUES (${channelCode}, ${channelName}, ${channelPlusType})
45 | ON CONFLICT (channelCode) DO
46 | UPDATE SET channelName = ${channelName}, channelPlusType = ${channelPlusType}
47 | `);
48 | return statement.changes;
49 | };
50 |
51 | exports.getChannelArchive = async function() {
52 | const db = await dbopen;
53 |
54 | const rows = await db.all(SQL`SELECT * FROM channelArchive`);
55 | return rows;
56 | };
57 |
58 | exports.addVliveChannel = async function(
59 | guildID,
60 | discordChanID,
61 | channelSeq,
62 | channelCode,
63 | channelName,
64 | mentionRoleID) {
65 | const db = await dbopen;
66 |
67 | const statement = await db.run(SQL`
68 | INSERT OR IGNORE
69 | INTO vliveChannels (guildID, discordChanID, channelSeq, channelCode, channelName, mentionRoleID)
70 | VALUES (${guildID}, ${discordChanID}, ${channelSeq}, ${channelCode}, ${channelName}, ${mentionRoleID})
71 | `);
72 | return statement.changes;
73 | };
74 |
75 | exports.removeVliveChannel = async function(discordChanID, channelSeq) {
76 | const db = await dbopen;
77 |
78 | const statement = await db.run(SQL`
79 | DELETE FROM vliveChannels
80 | WHERE discordChanID = ${discordChanID} AND channelSeq = ${channelSeq}
81 | `);
82 | return statement.changes;
83 | };
84 |
85 | exports.getVliveChannel = async function(discordChanID, channelSeq) {
86 | const db = await dbopen;
87 |
88 | const row = await db.get(SQL`
89 | SELECT * FROM vliveChannels
90 | WHERE discordChanID = ${discordChanID} AND channelSeq = ${channelSeq}
91 | `);
92 | return row;
93 | };
94 |
95 | exports.getGuildVliveChannels = async function(guildID) {
96 | const db = await dbopen;
97 |
98 | const rows = await db.all(SQL`SELECT * FROM vliveChannels WHERE guildID = ${guildID}`);
99 | return rows;
100 | };
101 |
102 | exports.getAllVliveChannels = async function() {
103 | const db = await dbopen;
104 |
105 | const rows = await db.all(SQL`SELECT * FROM vliveChannels`);
106 | return rows;
107 | };
108 |
109 | exports.toggleVpick = async function(discordChanID, channelSeq) {
110 | const db = await dbopen;
111 |
112 | const statement = await db.run(SQL`
113 | UPDATE OR IGNORE vliveChannels
114 | SET VPICK = ~VPICK & 1
115 | WHERE discordChanID = ${discordChanID} AND channelSeq = ${channelSeq}
116 | `);
117 |
118 | let toggle = 0;
119 | if (statement.changes) {
120 | const row = await db.get(SQL`SELECT VPICK FROM vliveChannels WHERE discordChanID = ${discordChanID} AND channelSeq = ${channelSeq}`);
121 | toggle = row ? row.VPICK : 0;
122 | }
123 | return toggle;
124 | };
125 |
126 | exports.addVideo = async function(videoSeq, channelSeq) {
127 | const db = await dbopen;
128 |
129 | const statement = await db.run(SQL`INSERT OR IGNORE INTO vliveVideos VALUES (${videoSeq}, ${channelSeq})`);
130 | return statement.changes;
131 | };
132 |
133 | exports.getChannelVliveVideos = async function(channelSeq) {
134 | const db = await dbopen;
135 |
136 | const rows = await db.all(SQL`SELECT * FROM vliveVideos WHERE channelSeq = ${channelSeq}`);
137 | return rows;
138 | };
139 |
140 | exports.getAllVliveVideos = async function() {
141 | const db = await dbopen;
142 |
143 | const rows = await db.all(SQL`SELECT * FROM vliveVideos`);
144 | return rows;
145 | };
146 |
--------------------------------------------------------------------------------
/db_queries/roles_db.js:
--------------------------------------------------------------------------------
1 | const sql = require('sqlite3').verbose();
2 | const db = new sql.Database('./haseul_data/roles.db');
3 |
4 | db.serialize(() => {
5 | db.run('CREATE TABLE IF NOT EXISTS rolesMessages (guildID TEXT NOT NULL, messageID TEXT, msg TEXT)');
6 | db.run('CREATE TABLE IF NOT EXISTS availableRoles (roleName TEXT NOT NULL, guildID TEXT NOT NULL, type TEXT NOT NULL)');
7 | db.run('CREATE TABLE IF NOT EXISTS roles (roleCommand TEXT NOT NULL, roleID NOT NULL, roleName TEXT NOT NULL, guildID TEXT NOT NULL, type TEXT NOT NULL)');
8 | });
9 |
10 | // Channel message
11 |
12 | exports.setMsgId = async (guildId, msgId) => new Promise((resolve, reject) => {
13 | db.run('UPDATE rolesMessages SET messageID = ? WHERE guildID = ?', [msgId, guildId], err => {
14 | if (err) return reject(err);
15 | return resolve();
16 | });
17 | });
18 |
19 | exports.setRolesMsg = (guildId, msg) => new Promise((resolve, reject) => {
20 | db.get('SELECT msg FROM rolesMessages WHERE guildID = ?', [guildId], (err, row) => {
21 | if (err) return reject(err);
22 | if (!row) {
23 | db.run('INSERT INTO rolesMessages (guildID, msg) VALUES (?,?)', [guildId, msg], err => {
24 | if (err) return reject(err);
25 | return resolve(false);
26 | });
27 | } else {
28 | db.run('UPDATE rolesMessages SET msg = ? WHERE guildID = ?', [msg, guildId], err => {
29 | if (err) return reject(err);
30 | return resolve(true);
31 | });
32 | }
33 | });
34 | });
35 |
36 | exports.getRolesMsg = guildId => new Promise((resolve, reject) => {
37 | db.get('SELECT * FROM rolesMessages WHERE guildID = ?', [guildId], (err, row) => {
38 | if (err) return reject(err);
39 | return resolve(row);
40 | });
41 | });
42 |
43 | // Role pairs
44 |
45 | exports.addRole = (
46 | roleCommand, roleId, roleName, guildId, type) => {
47 | return new Promise((resolve, reject) => {
48 | db.get('SELECT roleCommand FROM roles WHERE roleCommand = ? AND guildID = ? AND type = ?', [roleCommand.toLowerCase(), guildId, type.toUpperCase()], (err, row) => {
49 | if (err) return reject(err);
50 | if (row) return resolve(false);
51 | db.run('INSERT INTO roles VALUES (?, ?, ?, ?, ?)', [roleCommand.toLowerCase(), roleId, roleName, guildId, type.toUpperCase()], err => {
52 | if (err) return reject(err);
53 | return resolve(true);
54 | });
55 | });
56 | });
57 | };
58 |
59 | exports.removeRole = (
60 | roleCommand, guildId, type) => new Promise((resolve, reject) => {
61 | db.get('SELECT roleCommand FROM roles WHERE roleCommand = ? AND guildID = ? AND type = ?', [roleCommand.toLowerCase(), guildId, type.toUpperCase()], (err, row) => {
62 | if (err) return reject(err);
63 | if (!row) return resolve(false);
64 | db.run('DELETE FROM roles WHERE roleCommand = ? AND guildID = ? AND type = ?', [roleCommand.toLowerCase(), guildId, type.toUpperCase()], err => {
65 | if (err) return reject(err);
66 | return resolve(true);
67 | });
68 | });
69 | });
70 |
71 | exports.getAllRoles = guildId => new Promise((resolve, reject) => {
72 | db.all('SELECT roleCommand, roleID, type FROM roles WHERE guildID = ?', [guildId], (err, rows) => {
73 | if (err) return reject(err);
74 | return resolve(rows);
75 | });
76 | });
77 |
78 | exports.getRoleId = (
79 | roleCommand, guildId, type) => new Promise((resolve, reject) => {
80 | type = type.toUpperCase();
81 | db.get('SELECT roleID FROM roles WHERE roleCommand = ? AND guildID = ? AND type = ?', [roleCommand.toLowerCase(), guildId, type], (err, row) => {
82 | if (err) return reject(err);
83 | return resolve(row ? row.roleID : undefined);
84 | });
85 | });
86 |
87 | // Available roles
88 |
89 | exports.availableRoleToggle = (
90 | roleName, guildId, type) => new Promise((resolve, reject) => {
91 | db.get('SELECT * FROM availableRoles WHERE roleName = ? AND guildID = ? AND type = ?', [roleName, guildId, type.toUpperCase()], (err, row) => {
92 | if (err) return reject(err);
93 | if (row) {
94 | db.run('DELETE FROM availableRoles WHERE roleName = ? AND guildID = ? AND type = ?', [roleName, guildId, type.toUpperCase()], err => {
95 | if (err) return reject(err);
96 | return resolve([undefined, roleName]);
97 | });
98 | } else {
99 | db.run('INSERT INTO availableRoles VALUES (?, ?, ?)', [roleName, guildId, type.toUpperCase()], err => {
100 | if (err) return reject(err);
101 | return resolve([roleName, undefined]);
102 | });
103 | }
104 | });
105 | });
106 |
107 | exports.getAvailableRoles = guildId => new Promise((resolve, reject) => {
108 | db.all('SELECT roleName, type FROM availableRoles WHERE guildID = ?', [guildId], (err, rows) => {
109 | if (err) return reject(err);
110 | return resolve(rows);
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/functions/images.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const html = require('../functions/html.js');
3 |
4 | class Image {
5 | constructor(data) {
6 | this.image = Uint8Array.from(data);
7 | }
8 |
9 | get type() {
10 | const data = this.image;
11 | return (
12 | data.slice(0, 2).join(' ') == '255 216' ? 'jpg' :
13 | data.slice(0, 8).join(' ') == '137 80 78 71 13 10 26 10' ? 'png' :
14 | data.slice(0, 6).join(' ') == '71 73 70 56 57 97' ? 'gif' :
15 | null
16 | );
17 | }
18 |
19 | jpgDim(data) {
20 | const marker = data.findIndex((x, i) => x == 255 && data[i+1] == 192);
21 | const SOF0 = data.slice(marker, marker+13);
22 | this.imgHeight = SOF0
23 | .slice(5, 7)
24 | .reduce((sum, x, i) => sum + (x * (2 ** (16 - ((i+1)*8) ))), 0);
25 | this.imgWidth = SOF0
26 | .slice(7, 9)
27 | .reduce((sum, x, i) => sum + (x * (2 ** (16 - ((i+1)*8) ))), 0);
28 | return [this.imgWidth, this.imgHeight];
29 | }
30 |
31 | pngDim(data) {
32 | this.imgWidth = data
33 | .slice(16, 20)
34 | .reduce((sum, x, i) => sum + (x * (2 ** (32 - ((i+1)*8) ))), 0);
35 | this.imgHeight = data
36 | .slice(20, 24)
37 | .reduce((sum, x, i) => sum + (x * (2 ** (32 - ((i+1)*8) ))), 0);
38 | return [this.imgWidth, this.imgHeight];
39 | }
40 |
41 | gifDim(data) {
42 | this.imgWidth = data
43 | .slice(6, 8)
44 | .reduce((sum, x, i) => sum + (x * (2 ** (i*8) )), 0);
45 | this.imgHeight = data
46 | .slice(8, 10)
47 | .reduce((sum, x, i) => sum + (x * (2 ** (i*8) )), 0);
48 | return [this.imgWidth, this.imgHeight];
49 | }
50 |
51 | get dimensions() {
52 | if (!this.imgDims) {
53 | const data = this.image;
54 | const type = this.type;
55 | this.imgDims = (
56 | type == 'jpg' ? this.jpgDim(data) :
57 | type == 'png' ? this.pngDim(data) :
58 | type == 'gif' ? this.gifDim(data) :
59 | null
60 | );
61 | }
62 | return this.imgDims;
63 | }
64 |
65 | get width() {
66 | return this.imgWidth || this.dimensions[0];
67 | }
68 |
69 | get height() {
70 | return this.imgHeight || this.dimensions[1];
71 | }
72 | }
73 |
74 | async function createMediaCollage(media, width, height, col1w) {
75 | if (media.length < 2) {
76 | return media[0];
77 | }
78 |
79 | if (media.length > 4) {
80 | media = media.slice(0, 4);
81 | }
82 |
83 | if (media.length == 4) {
84 | const temp = media[1];
85 | media[1] = media[2];
86 | media[2] = temp;
87 | }
88 |
89 | const col1 = media.slice(0, Math.floor(media.length/2));
90 | const col2 = media.slice(Math.floor(media.length/2), 4);
91 |
92 | let htmlString = '';
93 | htmlString += '\n';
126 |
127 | const css = fs.readFileSync('./resources/css/twittermedia.css', { encoding: 'utf8' });
128 | htmlString = [
129 | '\n',
130 | '\n\n',
133 | '\n',
134 | `${htmlString}\n`,
135 | '\n\n',
136 | '\n',
137 | ].join('');
138 |
139 | const image = await html.toImage(htmlString, width, height, 'png');
140 | return image;
141 | }
142 |
143 | module.exports = { Image, createMediaCollage };
144 |
--------------------------------------------------------------------------------
/resources/JSON/languages.json:
--------------------------------------------------------------------------------
1 |
2 |
3 | {
4 | "af": {
5 | "name": "Afrikaans",
6 | "nativeName": "Afrikaans"
7 | },
8 | "ar": {
9 | "name": "Arabic",
10 | "nativeName": "العربية"
11 | },
12 | "bg": {
13 | "name": "Bulgarian",
14 | "nativeName": "Български"
15 | },
16 | "bn": {
17 | "name": "Bangla",
18 | "nativeName": "বাংলা"
19 | },
20 | "bs": {
21 | "name": "Bosnian",
22 | "nativeName": "bosanski (latinica)"
23 | },
24 | "ca": {
25 | "name": "Catalan",
26 | "nativeName": "Català"
27 | },
28 | "cs": {
29 | "name": "Czech",
30 | "nativeName": "Čeština"
31 | },
32 | "cy": {
33 | "name": "Welsh",
34 | "nativeName": "Welsh"
35 | },
36 | "da": {
37 | "name": "Danish",
38 | "nativeName": "Dansk"
39 | },
40 | "de": {
41 | "name": "German",
42 | "nativeName": "Deutsch"
43 | },
44 | "el": {
45 | "name": "Greek",
46 | "nativeName": "Ελληνικά"
47 | },
48 | "en": {
49 | "name": "English",
50 | "nativeName": "English"
51 | },
52 | "es": {
53 | "name": "Spanish",
54 | "nativeName": "Español"
55 | },
56 | "et": {
57 | "name": "Estonian",
58 | "nativeName": "Eesti"
59 | },
60 | "fa": {
61 | "name": "Persian",
62 | "nativeName": "Persian"
63 | },
64 | "fi": {
65 | "name": "Finnish",
66 | "nativeName": "Suomi"
67 | },
68 | "fil": {
69 | "name": "Filipino",
70 | "nativeName": "Filipino"
71 | },
72 | "fj": {
73 | "name": "Fijian",
74 | "nativeName": "Fijian"
75 | },
76 | "fr": {
77 | "name": "French",
78 | "nativeName": "Français"
79 | },
80 | "he": {
81 | "name": "Hebrew",
82 | "nativeName": "עברית"
83 | },
84 | "hi": {
85 | "name": "Hindi",
86 | "nativeName": "हिंदी"
87 | },
88 | "hr": {
89 | "name": "Croatian",
90 | "nativeName": "Hrvatski"
91 | },
92 | "ht": {
93 | "name": "Haitian Creole",
94 | "nativeName": "Haitian Creole"
95 | },
96 | "hu": {
97 | "name": "Hungarian",
98 | "nativeName": "Magyar"
99 | },
100 | "id": {
101 | "name": "Indonesian",
102 | "nativeName": "Indonesia"
103 | },
104 | "is": {
105 | "name": "Icelandic",
106 | "nativeName": "Íslenska"
107 | },
108 | "it": {
109 | "name": "Italian",
110 | "nativeName": "Italiano"
111 | },
112 | "ja": {
113 | "name": "Japanese",
114 | "nativeName": "日本語"
115 | },
116 | "ko": {
117 | "name": "Korean",
118 | "nativeName": "한국어"
119 | },
120 | "lt": {
121 | "name": "Lithuanian",
122 | "nativeName": "Lietuvių"
123 | },
124 | "lv": {
125 | "name": "Latvian",
126 | "nativeName": "Latviešu"
127 | },
128 | "mg": {
129 | "name": "Malagasy",
130 | "nativeName": "Malagasy"
131 | },
132 | "ms": {
133 | "name": "Malay",
134 | "nativeName": "Melayu"
135 | },
136 | "mt": {
137 | "name": "Maltese",
138 | "nativeName": "Il-Malti"
139 | },
140 | "mww": {
141 | "name": "Hmong Daw",
142 | "nativeName": "Hmong Daw"
143 | },
144 | "nb": {
145 | "name": "Norwegian",
146 | "nativeName": "Norsk"
147 | },
148 | "nl": {
149 | "name": "Dutch",
150 | "nativeName": "Nederlands"
151 | },
152 | "otq": {
153 | "name": "Querétaro Otomi",
154 | "nativeName": "Querétaro Otomi"
155 | },
156 | "pl": {
157 | "name": "Polish",
158 | "nativeName": "Polski"
159 | },
160 | "pt": {
161 | "name": "Portuguese",
162 | "nativeName": "Português"
163 | },
164 | "ro": {
165 | "name": "Romanian",
166 | "nativeName": "Română"
167 | },
168 | "ru": {
169 | "name": "Russian",
170 | "nativeName": "Русский"
171 | },
172 | "sk": {
173 | "name": "Slovak",
174 | "nativeName": "Slovenčina"
175 | },
176 | "sl": {
177 | "name": "Slovenian",
178 | "nativeName": "Slovenščina"
179 | },
180 | "sm": {
181 | "name": "Samoan",
182 | "nativeName": "Samoan"
183 | },
184 | "sr-Cyrl": {
185 | "name": "Serbian (Cyrillic)",
186 | "nativeName": "srpski (ćirilica)"
187 | },
188 | "sr-Latn": {
189 | "name": "Serbian (Latin)",
190 | "nativeName": "srpski (latinica)"
191 | },
192 | "sv": {
193 | "name": "Swedish",
194 | "nativeName": "Svenska"
195 | },
196 | "sw": {
197 | "name": "Kiswahili",
198 | "nativeName": "Kiswahili"
199 | },
200 | "ta": {
201 | "name": "Tamil",
202 | "nativeName": "தமிழ்"
203 | },
204 | "te": {
205 | "name": "Telugu",
206 | "nativeName": "తెలుగు"
207 | },
208 | "th": {
209 | "name": "Thai",
210 | "nativeName": "ไทย"
211 | },
212 | "tlh": {
213 | "name": "Klingon",
214 | "nativeName": "Klingon"
215 | },
216 | "to": {
217 | "name": "Tongan",
218 | "nativeName": "lea fakatonga"
219 | },
220 | "tr": {
221 | "name": "Turkish",
222 | "nativeName": "Türkçe"
223 | },
224 | "ty": {
225 | "name": "Tahitian",
226 | "nativeName": "Tahitian"
227 | },
228 | "uk": {
229 | "name": "Ukrainian",
230 | "nativeName": "Українська"
231 | },
232 | "ur": {
233 | "name": "Urdu",
234 | "nativeName": "اردو"
235 | },
236 | "vi": {
237 | "name": "Vietnamese",
238 | "nativeName": "Tiếng Việt"
239 | },
240 | "yua": {
241 | "name": "Yucatec Maya",
242 | "nativeName": "Yucatec Maya"
243 | },
244 | "yue": {
245 | "name": "Cantonese (Traditional)",
246 | "nativeName": "粵語 (繁體中文)"
247 | },
248 | "zh-Hans": {
249 | "name": "Chinese Simplified",
250 | "nativeName": "简体中文"
251 | },
252 | "zh-Hant": {
253 | "name": "Chinese Traditional",
254 | "nativeName": "繁體中文"
255 | }
256 | }
257 |
258 |
--------------------------------------------------------------------------------
/modules/message_logs.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const { Client } = require('../haseul.js');
3 | const { checkPermissions, resolveMember, withTyping } = require('../functions/discord.js');
4 | const filterCache = require('../utils/filter_cache');
5 |
6 | const serverSettings = require('../utils/server_settings.js');
7 | const { parseChannelID } = require('../functions/functions.js');
8 |
9 | const deleteColour = 0xf93437;
10 | const spamColour = 0xf74623;
11 | const editColour = 0xff9b35;
12 |
13 | exports.onCommand = async function(message, args) {
14 | const { channel, member } = message;
15 |
16 | switch (args[0]) {
17 | case 'messagelogs':
18 | case 'msglogs':
19 | switch (args[1]) {
20 | case 'channel':
21 | switch (args[2]) {
22 | case 'set':
23 | if (checkPermissions(member, ['MANAGE_CHANNELS'])) {
24 | withTyping(channel, setMsgLogsChannel, [message, args[3]]);
25 | }
26 | break;
27 | }
28 | break;
29 | case 'toggle':
30 | if (checkPermissions(member, ['MANAGE_GUILD'])) {
31 | withTyping(channel, toggleMsgLogs, [message]);
32 | }
33 | break;
34 | default:
35 | message.channel.send({ content: 'Help with message logs can be found here: https://haseulbot.xyz/#logs' });
36 | break;
37 | }
38 | break;
39 | }
40 | };
41 |
42 | exports.onMessageDelete = async function(message) {
43 | logDeletedMessage(message);
44 | };
45 |
46 | exports.onMessageEdit = async function(oldMessage, newMessage) {
47 | logEditedMessage(oldMessage, newMessage);
48 | };
49 |
50 | async function logDeletedMessage(message) {
51 | const { id, author, content, guild } = message;
52 |
53 | const logsOn = serverSettings.get(guild.id, 'msgLogsOn');
54 | if (!logsOn) return;
55 | const logChannelID = serverSettings.get(guild.id, 'msgLogsChan');
56 | const channel = Client.channels.cache.get(logChannelID);
57 | if (!channel) return;
58 |
59 | const proximityMessages = await message.channel.messages
60 | .fetch({ limit: 5, before: message.id });
61 | let proximityMessage = proximityMessages
62 | .find(msg => msg.author.id !== author.id);
63 | proximityMessage = proximityMessage || proximityMessages.first();
64 |
65 | const isSpam = filterCache.deletedSpamMsgs.includes(id);
66 | if (isSpam) {
67 | filterCache.deletedSpamMsgs = filterCache.deletedSpamMsgs
68 | .filter(mid => mid != id);
69 | }
70 |
71 | const embed = new Discord.MessageEmbed({
72 | author: { name: author.tag, icon_url: author.displayAvatarURL({ format: 'png', dynamic: true, size: 128 }) },
73 | title: isSpam ? 'Spam Message Deleted' : 'Message Deleted',
74 | color: isSpam ? spamColour : deleteColour,
75 | footer: { text: `#${message.channel.name}` },
76 | timestamp: message.deletedAt,
77 | });
78 |
79 | if (content.length > 0) {
80 | embed.addField('Content', content.length > 1024 ? content.slice(0, 1021) + '...' : content, false);
81 | }
82 |
83 | if (message.attachments.size > 0) {
84 | embed.addField('Attachments', message.attachments.map(file => file.url).join('\n'), false);
85 | }
86 |
87 | embed.addField('Message Area', `[Go To Area](${proximityMessage.url})`, false);
88 |
89 | if (isSpam) {
90 | embed.addField('User ID', author.id);
91 | }
92 |
93 | channel.send({ embeds: [embed] });
94 | }
95 |
96 | async function logEditedMessage(oldMessage, newMessage) {
97 | if (oldMessage.content === newMessage.content) return;
98 | const { author, guild } = oldMessage;
99 |
100 | const logsOn = serverSettings.get(guild.id, 'msgLogsOn');
101 | if (!logsOn) return;
102 | const logChannelID = serverSettings.get(guild.id, 'msgLogsChan');
103 | const channel = Client.channels.cache.get(logChannelID);
104 | if (!channel) return;
105 |
106 | const embed = new Discord.MessageEmbed({
107 | author: { name: author.tag, icon_url: author.displayAvatarURL({ format: 'png', dynamic: true, size: 128 }) },
108 | title: 'Message Edited',
109 | color: editColour,
110 | footer: { text: `#${newMessage.channel.name}` },
111 | timestamp: newMessage.editedAt,
112 | });
113 |
114 | if (oldMessage.content.length > 0) {
115 | embed.addField('Old Content', oldMessage.content.length > 1024 ? oldMessage.content.slice(0, 1021) + '...' : oldMessage.content, false);
116 | }
117 | if (newMessage.content.length > 0) {
118 | embed.addField('New Content', newMessage.content.length > 1024 ? newMessage.content.slice(0, 1021) + '...' : newMessage.content, false);
119 | }
120 |
121 | embed.addField('Message Link', `[View Message](${newMessage.url})`, false);
122 |
123 | channel.send({ embeds: [embed] });
124 | }
125 |
126 | async function setMsgLogsChannel(message, channelArg) {
127 | const { guild } = message;
128 |
129 | let channelID;
130 | if (!channelArg) {
131 | channelID = message.channel.id;
132 | } else {
133 | channelID = parseChannelID(channelArg);
134 | }
135 |
136 | if (!channelID) {
137 | message.channel.send({ content: '⚠ Invalid channel or channel ID.' });
138 | return;
139 | }
140 |
141 | const channel = guild.channels.cache.get(channelID);
142 | if (!channel) {
143 | message.channel.send({ content: '⚠ Channel doesn\'t exist in this server.' });
144 | return;
145 | }
146 |
147 | const member = await resolveMember(guild, Client.user.id);
148 | if (!member) {
149 | message.channel.send({ content: '⚠ Error occurred.' });
150 | return;
151 | }
152 |
153 | const botPerms = channel.permissionsFor(member);
154 | if (!botPerms.has('VIEW_CHANNEL', true)) {
155 | message.channel.send({ content: '⚠ I cannot see this channel!' });
156 | return;
157 | }
158 | if (!botPerms.has('SEND_MESSAGES', true)) {
159 | message.channel.send({ content: '⚠ I cannot send messages to this channel!' });
160 | return;
161 | }
162 |
163 | await serverSettings.set(message.guild.id, 'msgLogsChan', channelID);
164 | message.channel.send({ content: `Message logs channel set to <#${channelID}>.` });
165 | }
166 |
167 | async function toggleMsgLogs(message) {
168 | const tog = await serverSettings.toggle(message.guild.id, 'msgLogsOn');
169 | message.channel.send({ content: `Message logs turned ${tog ? 'on':'off'}.` });
170 | }
171 |
--------------------------------------------------------------------------------
/db_queries/notifications_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/notifications.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | dbopen.then(db => {
10 | db.run(SQL`
11 | CREATE TABLE IF NOT EXISTS globalNotifs(
12 | userID TEXT NOT NULL,
13 | keyword TEXT NOT NULL,
14 | keyexp TEXT NOT NULL,
15 | type TEXT DEFAULT "NORMAL",
16 | UNIQUE(userID, keyword)
17 | )
18 | `);
19 | db.run(SQL`
20 | CREATE TABLE IF NOT EXISTS localNotifs(
21 | guildID TEXT NOT NULL,
22 | userID TEXT NOT NULL,
23 | keyword TEXT NOT NULL,
24 | keyexp TEXT NOT NULL,
25 | type TEXT DEFAULT "NORMAL",
26 | UNIQUE(guildID, userID, keyword)
27 | )
28 | `);
29 | db.run(SQL`
30 | CREATE TABLE IF NOT EXISTS channelBlacklist(
31 | userID TEXT NOT NULL,
32 | channelID TEXT NOT NULL,
33 | UNIQUE(userID, channelID)
34 | )
35 | `);
36 | db.run(SQL`
37 | CREATE TABLE IF NOT EXISTS serverBlacklist(
38 | userID TEXT NOT NULL,
39 | guildID TEXT NOT NULL,
40 | UNIQUE(userID, guildID)
41 | )
42 | `);
43 | db.run(SQL`
44 | CREATE TABLE IF NOT EXISTS DnD(
45 | userID TEXT NOT NULL PRIMARY KEY
46 | )
47 | `);
48 | });
49 |
50 | exports.addGlobalNotif = async function(userID, keyword, keyexp, type) {
51 | const db = await dbopen;
52 |
53 | const statement = await db.run(SQL`
54 | INSERT OR IGNORE INTO globalNotifs
55 | VALUES (${userID}, ${keyword}, ${keyexp}, ${type})
56 | `);
57 | return statement.changes;
58 | };
59 |
60 | exports.addLocalNotif = async function(guildID, userID, keyword, keyexp, type) {
61 | const db = await dbopen;
62 |
63 | const statement = await db.run(SQL`
64 | INSERT OR IGNORE INTO localNotifs
65 | VALUES (${guildID}, ${userID}, ${keyword}, ${keyexp}, ${type})
66 | `);
67 | return statement.changes;
68 | };
69 |
70 | exports.removeGlobalNotif = async function(userID, keyword) {
71 | const db = await dbopen;
72 |
73 | const statement = await db.run(SQL`DELETE FROM globalNotifs WHERE userID = ${userID} AND keyword = ${keyword}`);
74 | return statement.changes;
75 | };
76 |
77 | exports.removeLocalNotif = async function(guildID, userID, keyword) {
78 | const db = await dbopen;
79 |
80 | const statement = await db.run(SQL`
81 | DELETE FROM localNotifs
82 | WHERE guildID = ${guildID} AND userID = ${userID} AND keyword = ${keyword}
83 | `);
84 | return statement.changes;
85 | };
86 |
87 | exports.getGlobalNotifs = async function(userID) {
88 | const db = await dbopen;
89 |
90 | const rows = await db.all(SQL`SELECT * FROM globalNotifs WHERE userID = ${userID}`);
91 | return rows;
92 | };
93 |
94 | exports.getLocalNotifs = async function(guildID, userID) {
95 | const db = await dbopen;
96 |
97 | const rows = await db.all(SQL`SELECT * FROM localNotifs WHERE guildID = ${guildID} AND userID = ${userID}`);
98 | return rows;
99 | };
100 |
101 | exports.getAllGlobalNotifs = async function() {
102 | const db = await dbopen;
103 |
104 | const rows = await db.all(SQL`SELECT * FROM globalNotifs`);
105 | return rows;
106 | };
107 |
108 | exports.getAllLocalNotifs = async function(guildID) {
109 | const db = await dbopen;
110 |
111 | const rows = await db.all(SQL`SELECT * FROM localNotifs WHERE guildID = ${guildID}`);
112 | return rows;
113 | };
114 |
115 | exports.clearGlobalNotifs = async function(userID) {
116 | const db = await dbopen;
117 |
118 | const statement = await db.run(SQL`DELETE FROM globalNotifs WHERE userID = ${userID}`);
119 | return statement.changes;
120 | };
121 |
122 | exports.clearLocalNotifs = async function(guildID, userID) {
123 | const db = await dbopen;
124 |
125 | const statement = await db.run(SQL`DELETE FROM localNotifs WHERE guildID = ${guildID} AND userID = ${userID}`);
126 | return statement.changes;
127 | };
128 |
129 | exports.clearAllLocalNotifs = async function(userID) {
130 | const db = await dbopen;
131 |
132 | const statement = await db.run(SQL`DELETE FROM localNotifs WHERE userID = ${userID}`);
133 | return statement.changes;
134 | };
135 |
136 | exports.toggleChannel = async function(userID, channelID) {
137 | const db = await dbopen;
138 |
139 | const statement = await db.run(SQL`INSERT OR IGNORE INTO channelBlacklist VALUES (${userID}, ${channelID})`);
140 | if (!statement.changes) {
141 | await db.run(SQL`DELETE FROM channelBlacklist WHERE userID = ${userID} AND channelID = ${channelID}`);
142 | }
143 | return statement.changes;
144 | };
145 |
146 | exports.getIgnoredChannels = async function() {
147 | const db = await dbopen;
148 |
149 | const rows = await db.all(SQL`SELECT * FROM channelBlacklist`);
150 | return rows;
151 | };
152 |
153 | exports.toggleServer = async function(userID, guildID) {
154 | const db = await dbopen;
155 |
156 | const statement = await db.run(SQL`INSERT OR IGNORE INTO serverBlacklist VALUES (${userID}, ${guildID})`);
157 | if (!statement.changes) {
158 | await db.run(SQL`DELETE FROM serverBlacklist WHERE userID = ${userID} AND guildID = ${guildID}`);
159 | }
160 | return statement.changes;
161 | };
162 |
163 | exports.includeServer = async function(userID, guildID) {
164 | const db = await dbopen;
165 |
166 | const statement = await db.run(SQL`DELETE FROM serverBlacklist WHERE userID = ${userID} AND guildID = ${guildID}`);
167 | return statement.changes;
168 | };
169 |
170 | exports.getIgnoredServers = async function() {
171 | const db = await dbopen;
172 |
173 | const rows = await db.all(SQL`SELECT * FROM serverBlacklist`);
174 | return rows;
175 | };
176 |
177 | exports.toggleDnD = async function(userID) {
178 | const db = await dbopen;
179 |
180 | const statement = await db.run(SQL`INSERT OR IGNORE INTO DnD VALUES (${userID})`);
181 | if (!statement.changes) {
182 | await db.run(SQL`DELETE FROM DnD WHERE userID = ${userID}`);
183 | }
184 | return statement.changes;
185 | };
186 |
187 | exports.getDnD = async function(userID) {
188 | const db = await dbopen;
189 |
190 | const row = await db.get(SQL`SELECT * FROM DnD WHERE userID = ${userID}`);
191 | return row;
192 | };
193 |
194 | exports.getAllDnD = async function() {
195 | const db = await dbopen;
196 |
197 | const rows = await db.all(SQL`SELECT * FROM DnD`);
198 | return rows.map(row => row.userID);
199 | };
200 |
--------------------------------------------------------------------------------
/modules/emojis.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js');
2 | const { embedPages, withTyping } = require('../functions/discord.js');
3 | const { Client } = require('../haseul.js');
4 |
5 | const axios = require('axios');
6 |
7 | exports.onCommand = async function(message, args) {
8 | const { channel } = message;
9 |
10 | switch (args[0]) {
11 | case 'emojis':
12 | case 'emoji':
13 | switch (args[1]) {
14 | case 'list':
15 | withTyping(channel, listEmojis, [message]);
16 | break;
17 | case 'search':
18 | withTyping(channel, searchEmojis, [message, args[2]]);
19 | break;
20 | case 'help':
21 | channel.send({ content: 'Help with emojis can be found here: https://haseulbot.xyz/#emoji' });
22 | break;
23 | default:
24 | withTyping(channel, largeEmoji, [message, args]);
25 | break;
26 | }
27 | break;
28 | }
29 | };
30 |
31 | exports.onMention = async function(message, args) {
32 | switch (args[0]) {
33 | case `<@${Client.user.id}>`:
34 | case `<@!${Client.user.id}>`:
35 | largeEmoji(message, args);
36 | break;
37 | }
38 | };
39 |
40 | async function listEmojis(message) {
41 | const { guild } = message;
42 | let emojis = guild.emojis.cache;
43 |
44 | if (emojis.size < 1) {
45 | message.channel.send({ content: '⚠ There are no emojis added to this server.' });
46 | return;
47 | }
48 |
49 | emojis = emojis.filter(x => x['_roles'].size < 1);
50 | const staticEmojis = emojis
51 | .filter(x => !x.animated).sort((a, b) => a.name.localeCompare(b.name));
52 | const animatedEmojis = emojis
53 | .filter(x => x.animated).sort((a, b) => a.name.localeCompare(b.name));
54 | let emojiString = staticEmojis.concat(animatedEmojis).map(x => `<${x.animated ? 'a':''}:${x.name}:${x.id}> \`:${x.name}:\`` + (x.animated ? ' (animated)' : '')).join('\n');
55 |
56 | const descriptions = [];
57 | while (emojiString.length > 2048 || emojiString.split('\n').length > 25) {
58 | let currString = emojiString.slice(0, 2048);
59 |
60 | let lastIndex = 0;
61 | for (let i = 0; i < 25; i++) {
62 | const index = currString.indexOf('\n', lastIndex) + 1;
63 | if (index) lastIndex = index; else break;
64 | }
65 | currString = currString.slice(0, lastIndex);
66 | emojiString = emojiString.slice(lastIndex);
67 |
68 | descriptions.push(currString);
69 | }
70 | descriptions.push(emojiString);
71 |
72 | const pages = descriptions.map((desc, i) => ({
73 | embeds: [{
74 | author: {
75 | name: `${emojis.length} Emoji${emojis.length != 1 ? 's':''} - ${staticEmojis.length} Static; ${animatedEmojis.length} Animated`, icon_url: 'https://i.imgur.com/hIpmRU2.png',
76 | },
77 | description: desc,
78 | color: 0xffcc4d,
79 | footer: {
80 | text: `Page ${i+1} of ${descriptions.length}`,
81 | },
82 | }],
83 | }));
84 |
85 | embedPages(message, pages);
86 | }
87 |
88 | async function searchEmojis(message, query) {
89 | if (!query) {
90 | message.channel.send({ content: '⚠ Please provide a search query.' });
91 | return;
92 | }
93 |
94 | const { guild } = message;
95 | const emojis = guild.emojis.cache
96 | .filter(x => x.name.toLowerCase().includes(query.toLowerCase()));
97 |
98 | if (emojis.size < 1) {
99 | message.channel.send({ content: `⚠ No results were found searching for "${query}".` });
100 | return;
101 | }
102 |
103 | let emojiString = emojis
104 | .sort((a, b) => a.name.localeCompare(b.name))
105 | .sort((a, b)=> {
106 | const diff = query.length / b.name.length -
107 | query.length / a.name.length;
108 | if (diff == 0) {
109 | return a.name.indexOf(query.toLowerCase()) -
110 | b.name.indexOf(query.toLowerCase());
111 | } else {
112 | return diff;
113 | }
114 | }).map(x => `<${x.animated ? 'a':''}:${x.name}:${x.id}> \`:${x.name}:\`` + (x.animated ? ' (animated)' : '')).join('\n');
115 |
116 | const descriptions = [];
117 | while (emojiString.length > 2048 || emojiString.split('\n').length > 25) {
118 | let currString = emojiString.slice(0, 2048);
119 |
120 | let lastIndex = 0;
121 | for (let i = 0; i < 25; i++) {
122 | const index = currString.indexOf('\n', lastIndex) + 1;
123 | if (index) lastIndex = index; else break;
124 | }
125 | currString = currString.slice(0, lastIndex);
126 | emojiString = emojiString.slice(lastIndex);
127 |
128 | descriptions.push(currString);
129 | }
130 | descriptions.push(emojiString);
131 |
132 | const pages = descriptions.map((desc, i) => ({
133 | embeds: [{
134 | author: {
135 | name: `${emojis.length} Result${emojis.length != 1 ? 's':''} Found for "${query.slice(0, 30)}"`, icon_url: 'https://i.imgur.com/hIpmRU2.png',
136 | },
137 | description: desc,
138 | color: 0xffcc4d,
139 | footer: {
140 | text: `Page ${i+1} of ${descriptions.length}`,
141 | },
142 | }],
143 | }));
144 |
145 | embedPages(message, pages);
146 | }
147 |
148 | async function largeEmoji(message, args) {
149 | if (args[0] == 'emoji' && args.length < 2) {
150 | message.channel.send({ content: '⚠ Please provide an emoji to enlarge.' });
151 | return;
152 | } else if (message.mentions.length < 1 || args.length < 2) {
153 | return;
154 | }
155 |
156 | const emojiMatch = args[1].match(/<(a)?:([^<>:]+):(\d+)>/i);
157 |
158 | if (!emojiMatch) {
159 | if (args[0] == 'emoji') {
160 | message.channel.send({ content: '⚠ Invalid emoji provided!' });
161 | }
162 | return;
163 | }
164 |
165 | message.channel.sendTyping();
166 | const animated = emojiMatch[1];
167 | const emojiName = emojiMatch[2];
168 | const emojiID = emojiMatch[3];
169 |
170 | const imageUrl = `https://cdn.discordapp.com/emojis/${emojiID}.${animated ? 'gif':'png'}`;
171 | const response = await axios.head(imageUrl);
172 | const imageType = response.headers['content-type'].split('/')[1];
173 | const imageSize = Math.max(Math.round(response.headers['content-length']/10)/100, 1/100);
174 |
175 | const embed = new Discord.MessageEmbed({
176 | title: `Emoji \`:${emojiName}:\``,
177 | image: { url: imageUrl },
178 | footer: { text: `Type: ${imageType.toUpperCase()} | Size: ${imageSize}KB` },
179 | });
180 |
181 | message.channel.send({ embeds: [embed] });
182 | }
183 |
--------------------------------------------------------------------------------
/modules/reminders.js:
--------------------------------------------------------------------------------
1 | const { withTyping } = require('../functions/discord.js');
2 | const { getDelta } = require('../functions/functions.js');
3 |
4 | const database = require('../db_queries/reminders_db.js');
5 |
6 | exports.onCommand = async function(message, args) {
7 | const { channel } = message;
8 |
9 | switch (args[0]) {
10 | case 'remind':
11 | switch (args[1]) {
12 | case 'me':
13 | withTyping(channel, setReminder, [message, args.slice(2)]);
14 | break;
15 | }
16 | break;
17 | case 'reminder':
18 | switch (args[1]) {
19 | case 'list':
20 | withTyping(channel, listReminders, [message, args]);
21 | break;
22 | }
23 | break;
24 | case 'reminders':
25 | switch (args[1]) {
26 | case 'list':
27 | withTyping(channel, listReminders, [message, args]);
28 | break;
29 | case 'clear':
30 | withTyping(channel, clearReminders, [message, args]);
31 | break;
32 | }
33 | break;
34 | case 'remindme':
35 | withTyping(channel, setReminder, [message, args.slice(1)]);
36 | break;
37 | }
38 | };
39 |
40 | async function setReminder(message, args) {
41 | message.delete();
42 | const startTimestamp = Date.now() / 1000; // seconds since 1970
43 |
44 | if (args.length < 4) {
45 | message.channel.send({ content: '⚠ Please provide a reminder and a time.' });
46 | return;
47 | }
48 |
49 | const { author, content } = message;
50 |
51 | let inPos = content.search(/(? 157680000) {
106 | message.channel.send({ content: '⚠ Reminder must be set less than 5 years into the future.' });
107 | return;
108 | }
109 |
110 | const lastID = await database
111 | .addReminder(author.id, remindContent, remindTimestamp, startTimestamp);
112 | if (!lastID) {
113 | message.channel.send({ content: '⚠ Error occurred.' });
114 | } else {
115 | try {
116 | await author.send({ content: `🔔 You will be reminded in \`${remindTimeString}\` to "${remindContent}"` });
117 | message.channel.send({ content: 'Reminder set.' });
118 | } catch (e) {
119 | if (e.code == 50007) {
120 | message.channel.send({ content: '⚠ I cannot send DMs to you. Please check your privacy settings and try again.' });
121 | database.removeReminder(lastID);
122 | }
123 | }
124 | }
125 | }
126 |
127 | async function listReminders(message, args) {
128 | const { author } = message;
129 | const reminders = await database.getUserReminders(author.id);
130 |
131 | if (reminders.length < 1) {
132 | message.channel.send({ content: '⚠ You don\'t have any reminders set!' });
133 | return;
134 | }
135 |
136 | const startTimestamp = Date.now() / 1000;
137 | const timeRemainingString = timeData => `${timeData.days}d ${timeData.hours}h ${timeData.minutes}m ${timeData.seconds}s`;
138 | let reminderString = ['**Reminder List**'].concat(reminders.map(reminder => `"${reminder.remindContent}" in ${timeRemainingString(getDelta((reminder.remindTimestamp - startTimestamp) * 1000, 'days'))}`)).join('\n');
139 |
140 | const pages = [];
141 | while (reminderString.length > 2048) {
142 | let currString = reminderString.slice(0, 2048);
143 |
144 | let lastIndex = 0;
145 | while (true) {
146 | const index = currString.indexOf('\n', lastIndex) + 1;
147 | if (index) lastIndex = index; else break;
148 | }
149 | currString = currString.slice(0, lastIndex);
150 | reminderString = reminderString.slice(lastIndex);
151 |
152 | pages.push(currString);
153 | }
154 | pages.push(reminderString);
155 |
156 | try {
157 | for (const page of pages) {
158 | await author.send(page);
159 | }
160 | message.channel.send({ content: 'A list of your notifications has been sent to your DMs.' });
161 | } catch (e) {
162 | if (e.code == 50007) {
163 | message.channel.send({ content: '⚠ I cannot send DMs to you. Please check your privacy settings and try again.' });
164 | }
165 | }
166 | }
167 |
168 | async function clearReminders(message, args) {
169 | const { author } = message;
170 | const changes = await database.clearUserReminders(author.id);
171 | if (!changes) {
172 | message.channel.send({ content: '⚠ No reminders to remove.' });
173 | } else {
174 | message.channel.send({ content: 'Reminders cleared.' });
175 | }
176 | }
177 |
178 |
--------------------------------------------------------------------------------
/db_queries/reps_db.js:
--------------------------------------------------------------------------------
1 | const sqlite = require('sqlite');
2 | const sqlite3 = require('sqlite3');
3 | const SQL = require('sql-template-strings');
4 | const dbopen = sqlite.open({
5 | filename: './haseul_data/reps.db',
6 | driver: sqlite3.Database,
7 | });
8 |
9 | function streakHash(IDarray) {
10 | IDarray = IDarray.sort();
11 | const longID = IDarray.join(';');
12 | const base64ID = Buffer.from(longID).toString('base64');
13 | return base64ID;
14 | }
15 |
16 | dbopen.then(db => {
17 | db.configure('busyTimeout', 10000);
18 | db.run(SQL`
19 | CREATE TABLE IF NOT EXISTS repProfiles(
20 | userID TEXT NOT NULL PRIMARY KEY,
21 | rep INT NOT NULL DEFAULT 0,
22 | repsRemaining INT NOT NULL DEFAULT 3,
23 | lastRepTimestamp INT
24 | )
25 | `);
26 | db.run(SQL`
27 | CREATE TABLE IF NOT EXISTS repStreaks(
28 | userHash TEXT NOT NULL PRIMARY KEY,
29 | user1 TEXT NOT NULL,
30 | user2 TEXT NOT NULL,
31 | firstRep INT NOT NULL,
32 | user1LastRep INT,
33 | user2LastRep INT
34 | )
35 | `);
36 | });
37 |
38 | exports.setUserReps = async function(userID, repsRemaining) {
39 | const db = await dbopen;
40 |
41 | const statement = await db.run(SQL`
42 | INSERT INTO repProfiles (userID, repsRemaining) VALUES (${userID}, ${repsRemaining})
43 | ON CONFLICT (userID) DO
44 | UPDATE SET repsRemaining = ${repsRemaining}
45 | `);
46 | return statement.changes;
47 | };
48 |
49 | exports.repUser = async function(senderID, recipientID, timestamp) {
50 | const db = await dbopen;
51 |
52 | const stmt1 = await db.run(SQL`
53 | INSERT INTO repProfiles (userID, repsRemaining, lastRepTimestamp)
54 | VALUES (${senderID}, 2, ${timestamp})
55 | ON CONFLICT (userID) DO
56 | UPDATE SET repsRemaining = repsRemaining - 1, lastRepTimestamp = ${timestamp}
57 | `);
58 | const stmt2 = await db.run(SQL`
59 | INSERT INTO repProfiles (userID, rep)
60 | VALUES (${recipientID}, 1)
61 | ON CONFLICT (userID) DO
62 | UPDATE SET rep = rep + 1
63 | `);
64 | return stmt1.changes + stmt2.changes;
65 | };
66 |
67 | exports.getRepProfile = async function(userID) {
68 | const db = await dbopen;
69 |
70 | const row = await db.get(SQL`SELECT * FROM repProfiles WHERE userID = ${userID}`);
71 | return row;
72 | };
73 |
74 | exports.getReps = async function() {
75 | const db = await dbopen;
76 |
77 | const rows = await db.all(SQL`SELECT * FROM repProfiles`);
78 | return rows;
79 | };
80 |
81 |
82 | exports.updateStreak = async function(senderID, recipientID, timestamp) {
83 | const db = await dbopen;
84 | const userHash = streakHash([senderID, recipientID]);
85 | let currentStreak = 0;
86 |
87 | const statement = await db.run(SQL`
88 | INSERT OR IGNORE INTO repStreaks
89 | VALUES (${userHash}, ${senderID}, ${recipientID}, ${timestamp}, ${timestamp}, NULL)
90 | `);
91 |
92 | if (!statement.changes) {
93 | const streak = await db.get(SQL`SELECT * FROM repStreaks WHERE userHash = ${userHash}`);
94 |
95 | if (streak) {
96 | const { userHash, firstRep, user1LastRep, user2LastRep } = streak;
97 | const sendingUser = Object
98 | .keys(streak)
99 | .find(key => streak[key] == senderID);
100 | const receivingUser = Object
101 | .keys(streak)
102 | .find(key => streak[key] == recipientID);
103 | const trailingTimestamp = Math
104 | .min(user1LastRep || firstRep, user2LastRep || firstRep);
105 | const leadingTimestamp = Math
106 | .max(user1LastRep || firstRep, user2LastRep || firstRep);
107 |
108 | if (timestamp - leadingTimestamp > 129600000) {/* 36 hours */
109 | await db.run(`
110 | UPDATE repStreaks
111 | SET firstRep = ?, ${sendingUser}LastRep = ?, ${receivingUser}LastRep = NULL
112 | WHERE userHash = ?`, [timestamp, timestamp, userHash],
113 | );
114 | } else if (timestamp - trailingTimestamp > 129600000) {/* 36 hours */
115 | const leadingUser = user1LastRep > user2LastRep ? 'user1' : 'user2';
116 | const receivingTimestamp = receivingUser == leadingUser ? streak[`${leadingUser}LastRep`] : null;
117 |
118 | await db.run(`
119 | UPDATE repStreaks
120 | SET firstRep = ${leadingUser}LastRep, ${sendingUser}LastRep = ?, ${receivingUser}LastRep = ?
121 | WHERE userHash = ?`, [timestamp, receivingTimestamp, userHash],
122 | );
123 | } else {
124 | await db.run(`
125 | UPDATE repStreaks
126 | SET ${sendingUser}LastRep = ?
127 | WHERE userHash = ?`, [timestamp, userHash],
128 | );
129 | currentStreak = Math
130 | .floor((timestamp - streak.firstRep) / 86400000); /* 24 Hours */
131 | }
132 | }
133 | }
134 |
135 | return currentStreak;
136 | };
137 |
138 | exports.updateStreaks = async function(timestamp) {
139 | const db = await dbopen;
140 |
141 | const streaks = await db.all(SQL`SELECT * FROM repStreaks`);
142 | for (const streak of streaks) {
143 | const { userHash, firstRep, user1LastRep, user2LastRep } = streak;
144 | const trailingTimestamp = Math
145 | .min(user1LastRep || firstRep, user2LastRep || firstRep);
146 | const leadingTimestamp = Math
147 | .max(user1LastRep || firstRep, user2LastRep || firstRep);
148 |
149 | if (timestamp - leadingTimestamp > 129600000) {/* 36 hours */
150 | await db.run(SQL`DELETE FROM repStreaks WHERE userHash = ${userHash}`);
151 | } else if (timestamp - trailingTimestamp > 129600000) {/* 36 hours */
152 | const [leadingUser, fallingUser] = user1LastRep > user2LastRep ? ['user1', 'user2'] : ['user2', 'user1'];
153 |
154 | await db.run(`
155 | UPDATE repStreaks
156 | SET firstRep = ${leadingUser}LastRep, ${fallingUser}LastRep = NULL
157 | WHERE userHash = ?`, [userHash],
158 | );
159 | }
160 | }
161 | };
162 |
163 | exports.getStreak = async function(senderID, recipientID) {
164 | const db = await dbopen;
165 | const userHash = streakHash([senderID, recipientID]);
166 |
167 | const row = await db.get(SQL`SELECT * FROM repStreaks WHERE userHash = ${userHash}`);
168 | return row;
169 | };
170 |
171 | exports.getUserStreaks = async function(userID) {
172 | const db = await dbopen;
173 |
174 | const rows = await db.all(SQL`SELECT * FROM repStreaks WHERE ${userID} IN (user1, user2)`);
175 | return rows;
176 | };
177 |
178 | exports.getAllStreaks = async function() {
179 | const db = await dbopen;
180 |
181 | const rows = await db.all(SQL`SELECT * FROM repStreaks`);
182 | return rows;
183 | };
184 |
--------------------------------------------------------------------------------
/resources/JSON/help.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "lastfm": {
4 | "name": "lastfm | fm",
5 | "description": "⚠ `[]` signifies the parameter is required, `<>` parameters and words ending with `?` are optional.",
6 | "image": "https://i.imgur.com/lQ3EqM6.png",
7 | "colour": "0xb90000",
8 | "commands": [
9 | {
10 | "name": "fm set",
11 | "value": "`.fm set [lastfm username]` this links your Discord account to your Last.fm account, meaning when you use other commands such as fm or fmyt you won't need to define a username."
12 | },
13 | {
14 | "name": "fm remove | delete",
15 | "value": "`.fm remove` this unlinks your Discord account from your Last.fm."
16 | },
17 | {
18 | "name": "fm recent | recents",
19 | "value": "`.fm recent