├── 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(/?/); 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(/?/); 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'; 94 | 95 | if (col1) { 96 | if (col1.length == 1) { 97 | if (col2.length > 1) { 98 | htmlString += `
\n`; 99 | } else { 100 | htmlString += `
\n`; 101 | } 102 | } 103 | if (col1.length == 2) { 104 | htmlString += '
\n'; 105 | for (let i=0; i<2; i++) { 106 | htmlString += `
\n`; 107 | } 108 | htmlString += '
\n'; 109 | } 110 | } 111 | 112 | if (col2) { 113 | if (col2.length == 1) { 114 | htmlString += `
\n`; 115 | } 116 | if (col2.length == 2) { 117 | htmlString += '
\n'; 118 | for (let i=0; i<2; i++) { 119 | htmlString += `
\n`; 120 | } 121 | htmlString += '
\n'; 122 | } 123 | } 124 | 125 | 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 ` this displays a defined amount of recent tracks you have listened to recently, displayed differently depending on if you request 1, 2, or more than 2 tracks to be displayed." 20 | }, 21 | { 22 | "name": "fm np", 23 | "value": "`.fm np ` this is shorthand for `.fm recent 1`" 24 | }, 25 | { 26 | "name": "fm", 27 | "value": "`.fm ` this is shorthand for `.fm recent 2`" 28 | }, 29 | { 30 | "name": "fmyt", 31 | "value": "`.fmyt` this searches YouTube for the song you're listening to/last listened to on Last.fm and returns it." 32 | }, 33 | { 34 | "name": "fm topartists | ta", 35 | "value": "`.fm topartists