Compare commits
61 Commits
snapshot/t
...
master
Author | SHA1 | Date |
---|---|---|
Alyxia Sother | 646198c7bc | |
Alyxia Sother | 7c7cd6c7cf | |
Alyxia Sother | 389aa35124 | |
Alyxia Sother | 5fc65e3c71 | |
Alyxia Sother | 3170d85376 | |
Alyxia Sother | e7cc94408b | |
Alyxia Sother | e7217cecb5 | |
smartfridge | 08e9ae5218 | |
Alyxia Sother | 69a8452574 | |
Alyxia Sother | 78f3490003 | |
Keanu Timmermans | 8093224c46 | |
Keanu Timmermans | 3751d01756 | |
Keanu Timmermans | 64dde60dab | |
Keanu Timmermans | ea58f3d52e | |
Alyxia Sother | ad82aef396 | |
Alyxia Sother | 0e66735565 | |
Alyxia Sother | 80deec025d | |
Alyxia Sother | 45cb482826 | |
Alyxia Sother | 4056c4ac0b | |
Alyxia Sother | 203c541025 | |
Alyxia Sother | fbb687d3d6 | |
WatDuhHekBro | 36bc488757 | |
Keanu Timmermans | 8ffbc367b1 | |
Alyxia Sother | 52c1420508 | |
Alyxia Sother | 985db250d9 | |
Dmytro Meleshko | 73278b7e88 | |
Dmytro Meleshko | 31c68a5d09 | |
WatDuhHekBro | e86abbef3e | |
EL2020 | 2c946c8558 | |
EL2020 | 3844a4929d | |
Keanu Timmermans | 2cb94cc6ac | |
WatDuhHekBro | 2969dfd814 | |
EL2020 | ce414cb266 | |
EL2020 | 62c5dd8602 | |
Keanu Timmermans | 1330b499c8 | |
WatDuhHekBro | 2040dbdefd | |
Alyxia Sother | ac81b6a103 | |
WatDuhHekBro | 1e673a3969 | |
WatDuhHekBro | 180acb318c | |
WatDuhHekBro | 9d4610249d | |
WatDuhHekBro | 077164ed23 | |
Alyxia Sother | 6003367a6b | |
Alyxia Sother | 58858c5d09 | |
WatDuhHekBro | f643f61f29 | |
WatDuhHekBro | 736070d615 | |
Keanu Timmermans | e249d4b86d | |
Keanu Timmermans | e844c61ece | |
Lexi Sother | 2c674cef95 | |
Keanu Timmermans | a44798edb1 | |
フズキ | fe9a4f9d7e | |
Keanu Timmermans | f0a342faec | |
WatDuhHekBro | 8c6ffb963e | |
WatDuhHekBro | 548969daba | |
WatDuhHekBro | 80fa59a433 | |
WatDuhHekBro | 81f6779068 | |
EL2020 | d2a558dff4 | |
MrHappyHam | a06ec300f7 | |
WatDuhHekBro | 576e55fbdf | |
Keanu Timmermans | c4b077757f | |
WatDuhHekBro | c8dadad450 | |
Keanu Timmermans | cc50aea4de |
|
@ -2,7 +2,7 @@ name: CodeQL + Docker Image
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- typescript
|
||||
- master
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
@ -59,3 +59,13 @@ jobs:
|
|||
docker buildx build \
|
||||
--tag keanucode/travbot-v3:latest \
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64 --push .
|
||||
|
||||
- name: Sync From Docker Hub to GitHub
|
||||
uses: onichandame/docker-registry-sync-action@master
|
||||
with:
|
||||
source_repository: docker.io/keanucode/travbot-v3:latest
|
||||
source_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
source_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
target_repository: docker.pkg.github.com/keanuplayz/travbot-v3/travbot-v3:latest
|
||||
target_username: ${{ secrets.GH_USERNAME }}
|
||||
target_password: ${{ secrets.GH_TOKEN }}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Specific to this repository
|
||||
dist/
|
||||
data/
|
||||
data/*
|
||||
data/public/emote-registry.json
|
||||
!data/public/
|
||||
tmp/
|
||||
test*
|
||||
!test/
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,3 +1,14 @@
|
|||
# 3.2.3
|
||||
- Fixed `info guild` bug on servers without an icon
|
||||
- Added non-pinging mention to `whois`
|
||||
- Moved location of emote registry
|
||||
- Added command to set default VC name
|
||||
- Added pat shop item
|
||||
- Reworked `say` command making use of webhooks to replicate ac2pic's Nitroless idea (Part 1)
|
||||
- Fixed `poll` duration
|
||||
- Fixed `eco pay` user searching
|
||||
- Fixed `admin set welcome type none`
|
||||
|
||||
# 3.2.2
|
||||
- Moved command handler code to [Onion Lasers](https://github.com/WatDuhHekBro/OnionLasers)
|
||||
- Reworked `poll`
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
# https://github.com/Automattic/node-canvas/issues/866#issuecomment-330001221
|
||||
# Took 7m 29s
|
||||
|
||||
FROM node:10.16.0-alpine
|
||||
FROM mhart/alpine-node:8.5.0
|
||||
FROM node:current-alpine
|
||||
FROM mhart/alpine-node:latest
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
|
@ -71,4 +71,4 @@ RUN npm i
|
|||
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "start"]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# TravBot-v3
|
||||
|
||||
<p align="center">
|
||||
<!-- The image could potentially be a hyperlink to invite TravBot. -->
|
||||
<img src="https://i.imgur.com/l2E2Tfi.png"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
|
@ -10,9 +11,6 @@
|
|||
<a href="https://github.com/keanuplayz/TravBot-v3/blob/master/CHANGELOG.md">
|
||||
<img src="https://img.shields.io/github/package-json/v/keanuplayz/travbot-v3" alt="Version">
|
||||
</a>
|
||||
<a href="https://github.com/keanuplayz/TravBot-v3/blob/typescript/CHANGELOG.md">
|
||||
<img src="https://img.shields.io/github/package-json/v/keanuplayz/travbot-v3/typescript" alt="Version (Dev)">
|
||||
</a>
|
||||
<a href="https://discord.js.org/">
|
||||
<img src="https://img.shields.io/github/package-json/dependency-version/keanuplayz/travbot-v3/discord.js" alt="Discord.js Version">
|
||||
</a>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
- [Setting up the development environment](#setting-up-the-development-environment)
|
||||
- [Adding a new command](#adding-a-new-command)
|
||||
- [Adding a new non-command feature](#adding-a-new-non-command-feature)
|
||||
- [Notes](#notes)
|
||||
|
||||
# Introduction
|
||||
|
||||
|
@ -16,6 +17,8 @@ This is a brief overview that'll describe where and how to add new features to T
|
|||
|
||||
*Note: Make sure to avoid using `npm run build`! This will remove all your dev dependencies (in order to reduce space used). Instead, use `npm run once` to compile and build in non-dev mode.*
|
||||
|
||||
*Note: `npm run dev` will automatically delete any leftover files, preventing errors that might occur because of it. However, sometimes you'd like to test stuff without that build step. To do that, run `npm run dev-fast`. You'll then have to manually delete the `dist` folder to clear any old files.*
|
||||
|
||||
*Note: If you update one of the APIs or utility functions, make sure to update the [documentation](Documentation.md).*
|
||||
|
||||
# Adding a new command
|
||||
|
@ -68,3 +71,15 @@ export const client = new Client();
|
|||
|
||||
import "./modules/myModule";
|
||||
```
|
||||
|
||||
# Notes
|
||||
|
||||
## Logger
|
||||
|
||||
All calls to `console.error`, `console.warn`, `console.log`, and `console.debug` will also add to an in-memory log you can download, noted by verbosity levels `Error`, `Warn`, `Info`, and `Verbose` respectively.
|
||||
- `Error`: This indicates stuff that could or is breaking at least some functionality of the bot.
|
||||
- `Warn`: This indicates stuff that should probably be fixed but isn't going to break the bot.
|
||||
- `Info`: Used for general events such as joining/leaving guilds for example, but try not to go overboard on logging everything.
|
||||
- `Verbose`: This is used as a sort of separator for logging potentially error-prone events so that if an error occurs, you can find the context that error originated from.
|
||||
- In order to make reading the logs easier, context should be provided with each call. For example, if a call is being made from the storage module, you'd do something like `console.log("[storage]", "the message")`.
|
||||
- If a message is clear enough as to what the context was though, it's probably unnecessary to include this prefix. However, you should definitely attach context prefixes to error objects, who knows where those might originate.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
|
@ -1,51 +1,53 @@
|
|||
{
|
||||
"name": "travebot",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"description": "TravBot Discord bot.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.prod.json && npm prune --production",
|
||||
"build": "rimraf dist && tsc --project tsconfig.prod.json && npm prune --production",
|
||||
"start": "node .",
|
||||
"once": "tsc && npm start",
|
||||
"dev": "tsc-watch --onSuccess \"node . dev\"",
|
||||
"dev": "tsc-watch --onSuccess \"npm run dev-instance\"",
|
||||
"dev-fast": "tsc-watch --onSuccess \"node . dev\"",
|
||||
"dev-instance": "rimraf dist && tsc && node . dev",
|
||||
"test": "jest",
|
||||
"format": "prettier --write **/*",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"canvas": "^2.7.0",
|
||||
"chalk": "^4.1.0",
|
||||
"discord.js": "^12.5.1",
|
||||
"discord.js-lavalink-lib": "^0.1.8",
|
||||
"figlet": "^1.5.0",
|
||||
"glob": "^7.1.6",
|
||||
"inquirer": "^7.3.3",
|
||||
"mathjs": "^9.3.0",
|
||||
"canvas": "^2.8.0",
|
||||
"chalk": "^4.1.2",
|
||||
"discord.js": "^13.3.0",
|
||||
"figlet": "^1.5.2",
|
||||
"glob": "^7.2.0",
|
||||
"inquirer": "^8.2.0",
|
||||
"moment": "^2.29.1",
|
||||
"ms": "^2.1.3",
|
||||
"onion-lasers": "^1.1.0",
|
||||
"node-wolfram-alpha": "^1.2.5",
|
||||
"onion-lasers": "npm:onion-lasers-v13@^2.2.1",
|
||||
"pet-pet-gif": "^1.0.9",
|
||||
"relevant-urban": "^2.0.0",
|
||||
"translate-google": "^1.4.3",
|
||||
"weather-js": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/figlet": "^1.5.0",
|
||||
"@types/glob": "^7.1.3",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/mathjs": "^6.0.11",
|
||||
"@types/figlet": "^1.5.4",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/inquirer": "^8.1.3",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/mathjs": "^9.4.1",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/ws": "^7.4.0",
|
||||
"husky": "^5.0.6",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "2.1.2",
|
||||
"ts-jest": "^26.4.4",
|
||||
"tsc-watch": "^4.2.9",
|
||||
"typescript": "^3.9.7"
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/ws": "^8.2.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.3.1",
|
||||
"prettier": "2.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^27.0.7",
|
||||
"tsc-watch": "^4.5.0",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.1.2"
|
||||
"fsevents": "^2.3.2"
|
||||
},
|
||||
"author": "Keanu Timmermans",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
const discord = require("discord.js");
|
||||
let bot = new discord.Client({
|
||||
intents: [
|
||||
discord.Intents.FLAGS.GUILDS,
|
||||
discord.Intents.FLAGS.GUILD_MEMBERS,
|
||||
discord.Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
|
||||
discord.Intents.FLAGS.GUILD_VOICE_STATES,
|
||||
discord.Intents.FLAGS.GUILD_PRESENCES,
|
||||
discord.Intents.FLAGS.GUILD_MESSAGES,
|
||||
discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
|
||||
discord.Intents.FLAGS.DIRECT_MESSAGES
|
||||
]
|
||||
});
|
||||
bot.login(require("./data/config.json").token);
|
|
@ -1,5 +1,6 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import figlet from "figlet";
|
||||
import {Util} from "discord.js";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Generates a figlet of your input.",
|
||||
|
@ -7,12 +8,11 @@ export default new NamedCommand({
|
|||
any: new RestCommand({
|
||||
async run({send, combined}) {
|
||||
return send(
|
||||
figlet.textSync(combined, {
|
||||
horizontalLayout: "full"
|
||||
}),
|
||||
{
|
||||
code: true
|
||||
}
|
||||
`\`\`\`\n${Util.cleanCodeBlockContent(
|
||||
figlet.textSync(combined, {
|
||||
horizontalLayout: "full"
|
||||
})
|
||||
)}\n\`\`\``
|
||||
);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,12 +3,12 @@ import {NamedCommand} from "onion-lasers";
|
|||
export default new NamedCommand({
|
||||
description: "Insult TravBot! >:D",
|
||||
async run({send, channel, author}) {
|
||||
channel.startTyping();
|
||||
channel.sendTyping();
|
||||
setTimeout(() => {
|
||||
send(
|
||||
`${author} What the fuck did you just fucking say about me, you little bitch? I'll have you know I graduated top of my class in the Navy Seals, and I've been involved in numerous secret raids on Al-Quaeda, and I have over 300 confirmed kills. I am trained in gorilla warfare and I'm the top sniper in the entire US armed forces. You are nothing to me but just another target. I will wipe you the fuck out with precision the likes of which has never been seen before on this Earth, mark my fucking words. You think you can get away with saying that shit to me over the Internet? Think again, fucker. As we speak I am contacting my secret network of spies across the USA and your IP is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. You're fucking dead, kid. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and that's just with my bare hands. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the United States Marine Corps and I will use it to its full extent to wipe your miserable ass off the face of the continent, you little shit. If only you could have known what unholy retribution your little "clever" comment was about to bring down upon you, maybe you would have held your fucking tongue. But you couldn't, you didn't, and now you're paying the price, you goddamn idiot. I will shit fury all over you and you will drown in it. You're fucking dead, kiddo.`
|
||||
);
|
||||
channel.stopTyping();
|
||||
channel.sendTyping();
|
||||
}, 60000);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ export default new NamedCommand({
|
|||
channelType: CHANNEL_TYPE.GUILD,
|
||||
async run({send, guild}) {
|
||||
const member = guild!.members.cache.random();
|
||||
send(`I love ${member.nickname ?? member.user.username}!`);
|
||||
if (!member) return send("For some reason, an error occurred fetching a member.");
|
||||
return send(`I love ${member.nickname ?? member.user.username}!`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -39,9 +39,15 @@ export const BetCommand = new NamedCommand({
|
|||
// handle invalid amount
|
||||
if (amount <= 0) return send("You must bet at least one Mon!");
|
||||
else if (sender.money < amount)
|
||||
return send("You don't have enough Mons for that.", getMoneyEmbed(author));
|
||||
return send({
|
||||
content: "You don't have enough Mons for that.",
|
||||
embeds: [getMoneyEmbed(author, true)]
|
||||
});
|
||||
else if (receiver.money < amount)
|
||||
return send("They don't have enough Mons for that.", getMoneyEmbed(target));
|
||||
return send({
|
||||
content: "They don't have enough Mons for that.",
|
||||
embeds: [getMoneyEmbed(target, true)]
|
||||
});
|
||||
|
||||
return send("How long until the bet ends?");
|
||||
} else return;
|
||||
|
@ -67,9 +73,15 @@ export const BetCommand = new NamedCommand({
|
|||
// handle invalid amount
|
||||
if (amount <= 0) return send("You must bet at least one Mon!");
|
||||
else if (sender.money < amount)
|
||||
return send("You don't have enough Mons for that.", getMoneyEmbed(author));
|
||||
return send({
|
||||
content: "You don't have enough Mons for that.",
|
||||
embeds: [getMoneyEmbed(author, true)]
|
||||
});
|
||||
else if (receiver.money < amount)
|
||||
return send("They don't have enough Mons for that.", getMoneyEmbed(target));
|
||||
return send({
|
||||
content: "They don't have enough Mons for that.",
|
||||
embeds: [getMoneyEmbed(target, true)]
|
||||
});
|
||||
|
||||
// handle invalid duration
|
||||
if (duration <= 0) return send("Invalid bet duration");
|
||||
|
@ -107,7 +119,7 @@ export const BetCommand = new NamedCommand({
|
|||
);
|
||||
|
||||
// Wait for the duration of the bet.
|
||||
return client.setTimeout(async () => {
|
||||
return setTimeout(async () => {
|
||||
// In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
|
||||
const sender = Storage.getUser(author.id);
|
||||
const receiver = Storage.getUser(target.id);
|
||||
|
|
|
@ -16,22 +16,31 @@ export const DailyCommand = new NamedCommand({
|
|||
user.lastReceived = now;
|
||||
Storage.save();
|
||||
send({
|
||||
embed: {
|
||||
title: "Daily Reward",
|
||||
description: "You received 1 Mon!",
|
||||
color: ECO_EMBED_COLOR
|
||||
}
|
||||
embeds: [
|
||||
{
|
||||
title: "Daily Reward",
|
||||
description: "You received 1 Mon!",
|
||||
color: ECO_EMBED_COLOR,
|
||||
fields: [
|
||||
{
|
||||
name: "New balance:",
|
||||
value: pluralise(user.money, "Mon", "s")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
} else
|
||||
send({
|
||||
embed: {
|
||||
title: "Daily Reward",
|
||||
description: `It's too soon to pick up your daily Mons. You have about ${(
|
||||
(user.lastReceived + 79200000 - now) /
|
||||
3600000
|
||||
).toFixed(1)} hours to go.`,
|
||||
color: ECO_EMBED_COLOR
|
||||
}
|
||||
embeds: [
|
||||
{
|
||||
title: "Daily Reward",
|
||||
description: `It's too soon to pick up your daily Mons. Try again at <t:${Math.floor(
|
||||
(user.lastReceived + 79200000) / 1000
|
||||
)}:t>.`,
|
||||
color: ECO_EMBED_COLOR
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -50,25 +59,27 @@ export const GuildCommand = new NamedCommand({
|
|||
}
|
||||
|
||||
send({
|
||||
embed: {
|
||||
title: `The Bank of ${guild!.name}`,
|
||||
color: ECO_EMBED_COLOR,
|
||||
fields: [
|
||||
{
|
||||
name: "Accounts",
|
||||
value: Object.keys(users).length,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Total Mons",
|
||||
value: totalAmount,
|
||||
inline: true
|
||||
embeds: [
|
||||
{
|
||||
title: `The Bank of ${guild!.name}`,
|
||||
color: ECO_EMBED_COLOR,
|
||||
fields: [
|
||||
{
|
||||
name: "Accounts",
|
||||
value: Object.keys(users).length.toString(),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Total Mons",
|
||||
value: totalAmount.toString(),
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
thumbnail: {
|
||||
url: guild?.iconURL() ?? ""
|
||||
}
|
||||
],
|
||||
thumbnail: {
|
||||
url: guild?.iconURL() ?? ""
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -95,14 +106,16 @@ export const LeaderboardCommand = new NamedCommand({
|
|||
}
|
||||
|
||||
send({
|
||||
embed: {
|
||||
title: "Top 10 Richest Players",
|
||||
color: ECO_EMBED_COLOR,
|
||||
fields: fields,
|
||||
thumbnail: {
|
||||
url: guild?.iconURL() ?? ""
|
||||
embeds: [
|
||||
{
|
||||
title: "Top 10 Richest Players",
|
||||
color: ECO_EMBED_COLOR,
|
||||
fields: fields,
|
||||
thumbnail: {
|
||||
url: guild?.iconURL() ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +138,10 @@ export const PayCommand = new NamedCommand({
|
|||
|
||||
if (amount <= 0) return send("You must send at least one Mon!");
|
||||
else if (sender.money < amount)
|
||||
return send("You don't have enough Mons for that.", getMoneyEmbed(author));
|
||||
return send({
|
||||
content: "You don't have enough Mons for that.",
|
||||
embeds: [getMoneyEmbed(author, true)]
|
||||
});
|
||||
else if (target.id === author.id) return send("You can't send Mons to yourself!");
|
||||
else if (target.bot && !IS_DEV_MODE) return send("You can't send Mons to a bot!");
|
||||
|
||||
|
@ -141,7 +157,7 @@ export const PayCommand = new NamedCommand({
|
|||
run: "You must use the format `eco pay <user> <amount>`!"
|
||||
}),
|
||||
any: new RestCommand({
|
||||
async run({send, args, author, channel, guild, combined}) {
|
||||
async run({send, args, author, channel, guild}) {
|
||||
if (isAuthorized(guild, channel)) {
|
||||
const last = args.pop();
|
||||
|
||||
|
@ -152,27 +168,34 @@ export const PayCommand = new NamedCommand({
|
|||
|
||||
if (amount <= 0) return send("You must send at least one Mon!");
|
||||
else if (sender.money < amount)
|
||||
return send("You don't have enough Mons to do that!", getMoneyEmbed(author));
|
||||
return send({
|
||||
content: "You don't have enough Mons to do that!",
|
||||
embeds: [getMoneyEmbed(author, true)]
|
||||
});
|
||||
else if (!guild)
|
||||
return send("You have to use this in a server if you want to send Mons with a username!");
|
||||
|
||||
const user = await getUserByNickname(combined, guild);
|
||||
// Do NOT use the combined parameter here, it won't account for args.pop() at the start.
|
||||
const user = await getUserByNickname(args.join(" "), guild);
|
||||
if (typeof user === "string") return send(user);
|
||||
else if (user.id === author.id) return send("You can't send Mons to yourself!");
|
||||
else if (user.bot && !IS_DEV_MODE) return send("You can't send Mons to a bot!");
|
||||
|
||||
const confirmed = await confirm(
|
||||
await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, {
|
||||
embed: {
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.tag,
|
||||
icon_url: user.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
await send({
|
||||
content: `Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`,
|
||||
embeds: [
|
||||
{
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.tag,
|
||||
icon_url: user.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
author.id
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ export const MondayCommand = new NamedCommand({
|
|||
user.money++;
|
||||
user.lastMonday = now.getTime();
|
||||
Storage.save();
|
||||
send("It is **Mon**day, my dudes.", getMoneyEmbed(author));
|
||||
send({content: "It is **Mon**day, my dudes.", embeds: [getMoneyEmbed(author, true)]});
|
||||
} else send("You've already claimed your **Mon**day reward for this week.");
|
||||
} else {
|
||||
const weekdayName = WEEKDAY[weekday];
|
||||
|
@ -47,7 +47,7 @@ export const AwardCommand = new NamedCommand({
|
|||
const user = Storage.getUser(target.id);
|
||||
user.money++;
|
||||
Storage.save();
|
||||
send(`1 Mon given to ${target.username}.`, getMoneyEmbed(target));
|
||||
send({content: `1 Mon given to ${target.username}.`, embeds: [getMoneyEmbed(target, true)]});
|
||||
} else {
|
||||
send("This command is restricted to the bean.");
|
||||
}
|
||||
|
@ -62,7 +62,10 @@ export const AwardCommand = new NamedCommand({
|
|||
const user = Storage.getUser(target.id);
|
||||
user.money += amount;
|
||||
Storage.save();
|
||||
send(`${pluralise(amount, "Mon", "s")} given to ${target.username}.`, getMoneyEmbed(target));
|
||||
send({
|
||||
content: `${pluralise(amount, "Mon", "s")} given to ${target.username}.`,
|
||||
embeds: [getMoneyEmbed(target, true)]
|
||||
});
|
||||
} else {
|
||||
send("You need to enter a number greater than 0.");
|
||||
}
|
||||
|
|
|
@ -11,18 +11,18 @@ export interface ShopItem {
|
|||
|
||||
export const ShopItems: ShopItem[] = [
|
||||
{
|
||||
cost: 1,
|
||||
cost: 3,
|
||||
title: "Hug",
|
||||
description: "Hug Monika.",
|
||||
description: "Hug Monica.",
|
||||
usage: "hug",
|
||||
run(message, cost) {
|
||||
message.channel.send(`Transaction of ${cost} Mon completed successfully. <@394808963356688394>`);
|
||||
message.channel.send(`Transaction of ${cost} Mons completed successfully. <@394808963356688394>`);
|
||||
}
|
||||
},
|
||||
{
|
||||
cost: 2,
|
||||
cost: 5,
|
||||
title: "Handholding",
|
||||
description: "Hold Monika's hand.",
|
||||
description: "Hold Monica's hand.",
|
||||
usage: "handhold",
|
||||
run(message, cost) {
|
||||
message.channel.send(`Transaction of ${cost} Mons completed successfully. <@394808963356688394>`);
|
||||
|
@ -31,19 +31,29 @@ export const ShopItems: ShopItem[] = [
|
|||
{
|
||||
cost: 1,
|
||||
title: "Cute",
|
||||
description: "Calls Monika cute.",
|
||||
description: "Calls Monica cute.",
|
||||
usage: "cute",
|
||||
run(message) {
|
||||
message.channel.send("<:MoniCheeseBlushRed:637513137083383826>");
|
||||
}
|
||||
},
|
||||
{
|
||||
cost: 3,
|
||||
cost: 2,
|
||||
title: "Pat",
|
||||
description: "Pat Monica's head.",
|
||||
usage: "pat",
|
||||
run(message, cost) {
|
||||
message.channel.send(`Transaction of ${cost} Mons completed successfully. <@394808963356688394>`);
|
||||
}
|
||||
},
|
||||
{
|
||||
cost: 15,
|
||||
title: "Laser Bridge",
|
||||
description: "Buys what is technically a laser bridge.",
|
||||
usage: "laser bridge",
|
||||
run(message) {
|
||||
message.channel.send(random(lines), {
|
||||
message.channel.send({
|
||||
content: random(lines),
|
||||
files: [
|
||||
{
|
||||
attachment:
|
||||
|
|
|
@ -3,13 +3,13 @@ import {pluralise, split} from "../../../lib";
|
|||
import {Storage, getPrefix} from "../../../structures";
|
||||
import {isAuthorized, ECO_EMBED_COLOR} from "./eco-utils";
|
||||
import {ShopItems, ShopItem} from "./eco-shop-items";
|
||||
import {EmbedField} from "discord.js";
|
||||
import {EmbedField, MessageEmbedOptions} from "discord.js";
|
||||
|
||||
export const ShopCommand = new NamedCommand({
|
||||
description: "Displays the list of items you can buy in the shop.",
|
||||
async run({send, guild, channel, author}) {
|
||||
if (isAuthorized(guild, channel)) {
|
||||
function getShopEmbed(selection: ShopItem[], title: string) {
|
||||
function getShopEmbed(selection: ShopItem[], title: string): MessageEmbedOptions {
|
||||
const fields: EmbedField[] = [];
|
||||
|
||||
for (const item of selection)
|
||||
|
@ -20,13 +20,11 @@ export const ShopCommand = new NamedCommand({
|
|||
});
|
||||
|
||||
return {
|
||||
embed: {
|
||||
color: ECO_EMBED_COLOR,
|
||||
title: title,
|
||||
fields: fields,
|
||||
footer: {
|
||||
text: "Mon Shop | TravBot Services"
|
||||
}
|
||||
color: ECO_EMBED_COLOR,
|
||||
title: title,
|
||||
fields: fields,
|
||||
footer: {
|
||||
text: "Mon Shop | TravBot Services"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -35,10 +33,14 @@ export const ShopCommand = new NamedCommand({
|
|||
const pageAmount = shopPages.length;
|
||||
|
||||
paginate(send, author.id, pageAmount, (page, hasMultiplePages) => {
|
||||
return getShopEmbed(
|
||||
shopPages[page],
|
||||
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
|
||||
);
|
||||
return {
|
||||
embeds: [
|
||||
getShopEmbed(
|
||||
shopPages[page],
|
||||
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
|
||||
)
|
||||
]
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {pluralise} from "../../../lib";
|
||||
import {Storage} from "../../../structures";
|
||||
import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js";
|
||||
import {User, Guild, TextChannel, DMChannel, NewsChannel, Channel, TextBasedChannel} from "discord.js";
|
||||
|
||||
export const ECO_EMBED_COLOR = 0xf1c40f;
|
||||
|
||||
export function getMoneyEmbed(user: User): object {
|
||||
export function getMoneyEmbed(user: User, inline: boolean = false): object {
|
||||
const profile = Storage.getUser(user.id);
|
||||
console.log(profile);
|
||||
|
||||
return {
|
||||
embed: {
|
||||
if (inline) {
|
||||
return {
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.username,
|
||||
|
@ -23,48 +24,83 @@ export function getMoneyEmbed(user: User): object {
|
|||
value: pluralise(profile.money, "Mon", "s")
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
embeds: [
|
||||
{
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.username,
|
||||
icon_url: user.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Balance",
|
||||
value: pluralise(profile.money, "Mon", "s")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getSendEmbed(sender: User, receiver: User, amount: number): object {
|
||||
return {
|
||||
embed: {
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: sender.username,
|
||||
icon_url: sender.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
},
|
||||
title: "Transaction",
|
||||
description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`,
|
||||
fields: [
|
||||
{
|
||||
name: `Sender: ${sender.tag}`,
|
||||
value: pluralise(Storage.getUser(sender.id).money, "Mon", "s")
|
||||
embeds: [
|
||||
{
|
||||
color: ECO_EMBED_COLOR,
|
||||
author: {
|
||||
name: sender.username,
|
||||
icon_url: sender.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
},
|
||||
{
|
||||
name: `Receiver: ${receiver.tag}`,
|
||||
value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s")
|
||||
title: "Transaction",
|
||||
description: `${sender.toString()} has sent ${pluralise(
|
||||
amount,
|
||||
"Mon",
|
||||
"s"
|
||||
)} to ${receiver.toString()}!`,
|
||||
fields: [
|
||||
{
|
||||
name: `Sender: ${sender.tag}`,
|
||||
value: pluralise(Storage.getUser(sender.id).money, "Mon", "s")
|
||||
},
|
||||
{
|
||||
name: `Receiver: ${receiver.tag}`,
|
||||
value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s")
|
||||
}
|
||||
],
|
||||
footer: {
|
||||
text: receiver.username,
|
||||
icon_url: receiver.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
}
|
||||
],
|
||||
footer: {
|
||||
text: receiver.username,
|
||||
icon_url: receiver.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export function isAuthorized(guild: Guild | null, channel: TextChannel | DMChannel | NewsChannel): boolean {
|
||||
if ((guild?.id === "637512823676600330" && channel?.id === "669464416420364288") || IS_DEV_MODE) return true;
|
||||
else {
|
||||
channel.send("Sorry, this command can only be used in Monika's emote server. (#mon-stocks)");
|
||||
export function isAuthorized(guild: Guild | null, channel: TextBasedChannel): boolean {
|
||||
if (IS_DEV_MODE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (guild?.id !== "637512823676600330") {
|
||||
channel.send("Sorry, this command can only be used in Monika's emote server.");
|
||||
return false;
|
||||
} else if (channel?.id !== "669464416420364288") {
|
||||
channel.send("Sorry, this command can only be used in <#669464416420364288>.");
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import {URL} from "url";
|
||||
import {Command, NamedCommand} from "onion-lasers";
|
||||
import {getContent} from "../../lib";
|
||||
|
||||
const endpoints: {sfw: {[key: string]: string}} = {
|
||||
sfw: {
|
||||
tickle: "/img/tickle",
|
||||
slap: "/img/slap",
|
||||
poke: "/img/poke",
|
||||
pat: "/img/pat",
|
||||
neko: "/img/neko",
|
||||
meow: "/img/meow",
|
||||
lizard: "/img/lizard",
|
||||
kiss: "/img/kiss",
|
||||
hug: "/img/hug",
|
||||
foxGirl: "/img/fox_girl",
|
||||
feed: "/img/feed",
|
||||
cuddle: "/img/cuddle",
|
||||
why: "/why",
|
||||
catText: "/cat",
|
||||
fact: "/fact",
|
||||
nekoGif: "/img/ngif",
|
||||
kemonomimi: "/img/kemonomimi",
|
||||
holo: "/img/holo",
|
||||
smug: "/img/smug",
|
||||
baka: "/img/baka",
|
||||
woof: "/img/woof",
|
||||
spoiler: "/spoiler",
|
||||
wallpaper: "/img/wallpaper",
|
||||
goose: "/img/goose",
|
||||
gecg: "/img/gecg",
|
||||
avatar: "/img/avatar",
|
||||
waifu: "/img/waifu"
|
||||
}
|
||||
};
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Provides you with a random image with the selected argument.",
|
||||
async run({send}) {
|
||||
send(`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.`);
|
||||
},
|
||||
any: new Command({
|
||||
description: "Image type to send.",
|
||||
async run({send, args}) {
|
||||
const arg = args[0];
|
||||
if (!(arg in endpoints.sfw)) return send("Couldn't find that endpoint!");
|
||||
let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`);
|
||||
const content = await getContent(url.toString());
|
||||
return send(content.url);
|
||||
}
|
||||
})
|
||||
});
|
|
@ -1,15 +1,21 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import {getContent} from "../../lib";
|
||||
import {URL} from "url";
|
||||
import {random} from "../../lib";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "OwO-ifies the input.",
|
||||
run: "You need to specify some text to owoify.",
|
||||
any: new RestCommand({
|
||||
async run({send, combined}) {
|
||||
let url = new URL(`https://nekos.life/api/v2/owoify?text=${combined}`);
|
||||
const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}.
|
||||
send(content.owo);
|
||||
// Copied from <https://github.com/Nekos-life/neko-website/blob/78b2532de2d91375d6de45e4446fc766ba169472/app.py#L78-L87>.
|
||||
const faces = ["owo", "UwU", ">w<", "^w^"];
|
||||
const owoified = combined
|
||||
.replace(/[rl]/g, "w")
|
||||
.replace(/[RL]/g, "W")
|
||||
.replace(/ove/g, "uv")
|
||||
.replace(/n/g, "ny")
|
||||
.replace(/N/g, "NY")
|
||||
.replace(/\!/g, ` ${random(faces)} `);
|
||||
send(owoified);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import {MessageAttachment, User} from "discord.js";
|
||||
import {NamedCommand, Command, RestCommand, getUserByNickname} from "onion-lasers";
|
||||
import petPetGif from "pet-pet-gif";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Generates a pat GIF of the provided attachment image OR the avatar of the mentioned user.",
|
||||
usage: "(@user)",
|
||||
async run({message, send, author}) {
|
||||
if (message.attachments.size !== 0) {
|
||||
const attachment = message.attachments.first()!;
|
||||
const gif = await petPetGif(attachment.url);
|
||||
const file = new MessageAttachment(gif, "pat.gif");
|
||||
send({files: [file]});
|
||||
} else {
|
||||
const gif = await petPetGif(author.displayAvatarURL({format: "png"}));
|
||||
const file = new MessageAttachment(gif, "pat.gif");
|
||||
send({files: [file]});
|
||||
}
|
||||
},
|
||||
id: "user",
|
||||
user: new Command({
|
||||
description: "User to generate a GIF of.",
|
||||
async run({send, args}) {
|
||||
const user: User = args[0];
|
||||
const gif = await petPetGif(user.displayAvatarURL({format: "png"}));
|
||||
const file = new MessageAttachment(gif, "pat.gif");
|
||||
send({files: [file]});
|
||||
}
|
||||
}),
|
||||
any: new RestCommand({
|
||||
description: "User to generate a GIF of.",
|
||||
async run({send, combined, guild}) {
|
||||
const user = await getUserByNickname(combined, guild);
|
||||
|
||||
if (typeof user === "string") send(user);
|
||||
else {
|
||||
const gif = await petPetGif(user.displayAvatarURL({format: "png"}));
|
||||
const file = new MessageAttachment(gif, "pat.gif");
|
||||
send({files: [file]});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
|
@ -12,7 +12,7 @@ export default new NamedCommand({
|
|||
any: new RestCommand({
|
||||
description: "Question for the poll.",
|
||||
async run({send, message, author, args, combined}) {
|
||||
execPoll(send, message, author, combined, args[0]);
|
||||
execPoll(send, message, author, combined, args[0] * 1000);
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
@ -33,26 +33,35 @@ async function execPoll(send: SendFunction, message: Message, user: User, questi
|
|||
dynamic: true,
|
||||
size: 2048
|
||||
}) || user.defaultAvatarURL;
|
||||
const msg = await send(
|
||||
new MessageEmbed()
|
||||
.setAuthor(`Poll created by ${message.author.username}`, icon)
|
||||
.setColor(0xffffff)
|
||||
.setFooter("React to vote.")
|
||||
.setDescription(question)
|
||||
);
|
||||
const msg = await send({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setAuthor(`Poll created by ${message.author.username}`, icon)
|
||||
.setColor(0xffffff)
|
||||
.setFooter("React to vote.")
|
||||
.setDescription(question)
|
||||
]
|
||||
});
|
||||
const results = await poll(msg, [AGREE, DISAGREE], duration);
|
||||
send(
|
||||
new MessageEmbed()
|
||||
.setAuthor(`The results of ${message.author.username}'s poll:`, icon)
|
||||
.setTitle(question)
|
||||
.setDescription(
|
||||
`${AGREE} - ${pluralise(
|
||||
results[AGREE],
|
||||
"",
|
||||
"people who agree",
|
||||
"person who agrees"
|
||||
)}\n${DISAGREE} - ${pluralise(results[DISAGREE], "", "people who disagree", "person who disagrees")}`
|
||||
)
|
||||
);
|
||||
send({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setAuthor(`The results of ${message.author.username}'s poll:`, icon)
|
||||
.setTitle(question)
|
||||
.setDescription(
|
||||
`${AGREE} - ${pluralise(
|
||||
results[AGREE],
|
||||
"",
|
||||
"people who agree",
|
||||
"person who agrees"
|
||||
)}\n${DISAGREE} - ${pluralise(
|
||||
results[DISAGREE],
|
||||
"",
|
||||
"people who disagree",
|
||||
"person who disagrees"
|
||||
)}`
|
||||
)
|
||||
]
|
||||
});
|
||||
msg.delete();
|
||||
}
|
||||
|
|
|
@ -6,15 +6,17 @@ export default new NamedCommand({
|
|||
usage: "[number from 1 to 9]",
|
||||
async run({send}) {
|
||||
send({
|
||||
embed: {
|
||||
title: "Ravioli ravioli...",
|
||||
image: {
|
||||
url: `https://raw.githubusercontent.com/keanuplayz/TravBot/master/assets/ravi${Random.int(
|
||||
1,
|
||||
10
|
||||
)}.png`
|
||||
embeds: [
|
||||
{
|
||||
title: "Ravioli ravioli...",
|
||||
image: {
|
||||
url: `https://raw.githubusercontent.com/keanuplayz/TravBot/master/assets/ravi${Random.int(
|
||||
1,
|
||||
10
|
||||
)}.png`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
number: new Command({
|
||||
|
@ -23,12 +25,14 @@ export default new NamedCommand({
|
|||
|
||||
if (arg >= 1 && arg <= 9) {
|
||||
send({
|
||||
embed: {
|
||||
title: "Ravioli ravioli...",
|
||||
image: {
|
||||
url: `https://raw.githubusercontent.com/keanuplayz/TravBot/master/assets/ravi${arg}.png`
|
||||
embeds: [
|
||||
{
|
||||
title: "Ravioli ravioli...",
|
||||
image: {
|
||||
url: `https://raw.githubusercontent.com/keanuplayz/TravBot/master/assets/ravi${arg}.png`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
send("Please provide a number between 1 and 9.");
|
||||
|
|
|
@ -36,25 +36,25 @@ export default new NamedCommand({
|
|||
usage: "([text])",
|
||||
async run({send, author}) {
|
||||
const msg = await send(transform(phrase));
|
||||
msg.createReactionCollector(
|
||||
(reaction, user) => {
|
||||
msg.createReactionCollector({
|
||||
filter: (reaction, user) => {
|
||||
if (user.id === author.id && reaction.emoji.name === "❌") msg.delete();
|
||||
return false;
|
||||
},
|
||||
{time: 60000}
|
||||
);
|
||||
time: 60000
|
||||
});
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, author, combined}) {
|
||||
phrase = combined;
|
||||
const msg = await send(transform(phrase));
|
||||
msg.createReactionCollector(
|
||||
(reaction, user) => {
|
||||
msg.createReactionCollector({
|
||||
filter: (reaction, user) => {
|
||||
if (user.id === author.id && reaction.emoji.name === "❌") msg.delete();
|
||||
return false;
|
||||
},
|
||||
{time: 60000}
|
||||
);
|
||||
time: 60000
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ export default new NamedCommand({
|
|||
if (res.tags && res.tags.length > 0 && res.tags.join(" ").length < 1024)
|
||||
embed.addField("Tags", res.tags.join(", "), true);
|
||||
|
||||
send(embed);
|
||||
send({embeds: [embed]});
|
||||
})
|
||||
.catch(() => {
|
||||
send("Sorry, that word was not found.");
|
||||
|
|
|
@ -29,7 +29,7 @@ export default new NamedCommand({
|
|||
.addField("Winds", current.winddisplay, true)
|
||||
.addField("Humidity", `${current.humidity}%`, true);
|
||||
return send({
|
||||
embed
|
||||
embeds: [embed]
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -39,8 +39,15 @@ const registry: {[id: string]: string} = {
|
|||
"389178357302034442": "In his dreams, he is the star. its him. <:itsMe:808174425253871657>",
|
||||
"606395763404046349": "Me.",
|
||||
"237359961842253835": "Good question.",
|
||||
"320680803124248576":
|
||||
"The resident meat lump and certified non-weeb. Inquire directly for details and keep that honey glaze to yourself.",
|
||||
"689538764950994990":
|
||||
"The slayer of memes, a vigilante of the voidborn, and the self-proclaimed prophet of Xereptheí.\n> And thus, I shall remain dormant once more. For when judgement day arrives, those whose names are sung shall pierce the heavens."
|
||||
"The slayer of memes, a vigilante of the voidborn, and the self-proclaimed prophet of Xereptheí.\n> And thus, I shall remain dormant once more. For when judgement day arrives, those whose names are sung shall pierce the heavens.",
|
||||
"273599683132260354":
|
||||
"Does memes, art crimes, programming, programming accessories, and is accessory to meme, programming, and art crimes. Also, tiny potato.",
|
||||
"156532969119547393": "Someone pretty cool for a bird made out of fire.",
|
||||
"388522171393245184": "The bat. Likes pats. If mean, apply whacks. 🗞️",
|
||||
"138840343855497216": "your face is a whois entry"
|
||||
};
|
||||
|
||||
export default new NamedCommand({
|
||||
|
@ -50,7 +57,7 @@ export default new NamedCommand({
|
|||
const id = author.id;
|
||||
|
||||
if (id in registry) {
|
||||
send(registry[id]);
|
||||
send({content: `${author} ${registry[id]}`, allowedMentions: {parse: []}});
|
||||
} else {
|
||||
send("You haven't been added to the registry yet!");
|
||||
}
|
||||
|
@ -62,9 +69,9 @@ export default new NamedCommand({
|
|||
const id = user.id;
|
||||
|
||||
if (id in registry) {
|
||||
send(registry[id]);
|
||||
send({content: `${user} ${registry[id]}`, allowedMentions: {parse: []}});
|
||||
} else {
|
||||
send(`\`${user.tag}\` hasn't been added to the registry yet!`);
|
||||
send({content: `${user} hasn't been added to the registry yet!`, allowedMentions: {parse: []}});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@ -74,9 +81,9 @@ export default new NamedCommand({
|
|||
|
||||
if (typeof user !== "string") {
|
||||
if (user.id in registry) {
|
||||
send(registry[user.id]);
|
||||
send({content: `${user} ${registry[user.id]}`, allowedMentions: {parse: []}});
|
||||
} else {
|
||||
send(`\`${user.tag}\` hasn't been added to the registry yet!`);
|
||||
send({content: `${user} hasn't been added to the registry yet!`, allowedMentions: {parse: []}});
|
||||
}
|
||||
} else {
|
||||
send(user);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {Command, NamedCommand, getPermissionLevel, getPermissionName, CHANNEL_TYPE, RestCommand} from "onion-lasers";
|
||||
import {clean} from "../../lib";
|
||||
import {Config, Storage} from "../../structures";
|
||||
import {Permissions, TextChannel, User, Role, Channel} from "discord.js";
|
||||
import {Permissions, TextChannel, User, Role, Channel, Util} from "discord.js";
|
||||
import {logs} from "../../modules/globals";
|
||||
|
||||
function getLogBuffer(type: string) {
|
||||
|
@ -60,6 +59,54 @@ export default new NamedCommand({
|
|||
})
|
||||
})
|
||||
}),
|
||||
messageembeds: new NamedCommand({
|
||||
description: "Enable or disable sending message previews.",
|
||||
usage: "enable/disable",
|
||||
run: "Please specify `enable` or `disable`.",
|
||||
subcommands: {
|
||||
true: new NamedCommand({
|
||||
description: "Enable sending of message previews.",
|
||||
async run({send, guild}) {
|
||||
Storage.getGuild(guild!.id).messageEmbeds = true;
|
||||
Storage.save();
|
||||
send("Sending of message previews has been enabled.");
|
||||
}
|
||||
}),
|
||||
false: new NamedCommand({
|
||||
description: "Disable sending of message previews.",
|
||||
async run({send, guild}) {
|
||||
Storage.getGuild(guild!.id).messageEmbeds = false;
|
||||
Storage.save();
|
||||
send("Sending of message previews has been disabled.");
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
autoroles: new NamedCommand({
|
||||
description: "Configure your server's autoroles.",
|
||||
usage: "<roles...>",
|
||||
async run({send, guild}) {
|
||||
Storage.getGuild(guild!.id).autoRoles = [];
|
||||
Storage.save();
|
||||
send("Reset this server's autoroles.");
|
||||
},
|
||||
id: "role",
|
||||
any: new RestCommand({
|
||||
description: "The roles to set as autoroles.",
|
||||
async run({send, guild, args}) {
|
||||
const guildd = Storage.getGuild(guild!.id);
|
||||
for (const role of args) {
|
||||
if (!role.toString().match(/^<@&(\d{17,})>$/)) {
|
||||
return send("Not all arguments are a role mention!");
|
||||
}
|
||||
const id = role.toString().match(/^<@&(\d{17,})>$/)![1];
|
||||
guildd.autoRoles!.push(id);
|
||||
}
|
||||
Storage.save();
|
||||
return send("Saved.");
|
||||
}
|
||||
})
|
||||
}),
|
||||
welcome: new NamedCommand({
|
||||
description: "Configure your server's welcome settings for the bot.",
|
||||
usage: "type/channel <...>",
|
||||
|
@ -89,6 +136,13 @@ export default new NamedCommand({
|
|||
Storage.save();
|
||||
send("Set this server's welcome type to `graphical`.");
|
||||
}
|
||||
}),
|
||||
none: new NamedCommand({
|
||||
async run({send, guild}) {
|
||||
Storage.getGuild(guild!.id).welcomeType = "none";
|
||||
Storage.save();
|
||||
send("Set this server's welcome type to `none`.");
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
@ -204,6 +258,36 @@ export default new NamedCommand({
|
|||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
name: new NamedCommand({
|
||||
aliases: ["defaultname"],
|
||||
description:
|
||||
"Sets the name that the channel will be reset to once no more members are in the channel.",
|
||||
usage: "(<name>)",
|
||||
async run({send, guild, message}) {
|
||||
const voiceChannel = message.member?.voice.channel;
|
||||
if (!voiceChannel) return send("You are not in a voice channel.");
|
||||
const guildStorage = Storage.getGuild(guild!.id);
|
||||
delete guildStorage.channelNames[voiceChannel.id];
|
||||
Storage.save();
|
||||
return send(`Successfully removed the default channel name for ${voiceChannel}.`);
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, guild, message, combined}) {
|
||||
const voiceChannel = message.member?.voice.channel;
|
||||
const guildID = guild!.id;
|
||||
const guildStorage = Storage.getGuild(guildID);
|
||||
const newName = combined;
|
||||
|
||||
if (!voiceChannel) return send("You are not in a voice channel.");
|
||||
if (!guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS))
|
||||
return send("I can't change channel names without the `Manage Channels` permission.");
|
||||
|
||||
guildStorage.channelNames[voiceChannel.id] = newName;
|
||||
Storage.save();
|
||||
return await send(`Set default channel name to "${newName}".`);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
@ -252,18 +336,18 @@ export default new NamedCommand({
|
|||
channelType: CHANNEL_TYPE.GUILD,
|
||||
async run({send, message, channel, guild, client}) {
|
||||
// It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES.
|
||||
if (guild!.me?.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES)) {
|
||||
if (guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES)) {
|
||||
message.delete();
|
||||
const msgs = await channel.messages.fetch({
|
||||
limit: 100
|
||||
});
|
||||
const travMessages = msgs.filter((m) => m.author.id === client.user?.id);
|
||||
|
||||
await send(`Found ${travMessages.size} messages to delete.`).then((m) =>
|
||||
m.delete({
|
||||
timeout: 5000
|
||||
})
|
||||
);
|
||||
await send(`Found ${travMessages.size} messages to delete.`).then((m) => {
|
||||
setTimeout(() => {
|
||||
m.delete();
|
||||
}, 5000);
|
||||
});
|
||||
await (channel as TextChannel).bulkDelete(travMessages);
|
||||
} else {
|
||||
send("This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission.");
|
||||
|
@ -286,34 +370,43 @@ export default new NamedCommand({
|
|||
}
|
||||
})
|
||||
}),
|
||||
eval: new NamedCommand({
|
||||
description: "Evaluate code.",
|
||||
usage: "<code>",
|
||||
permission: PERMISSIONS.BOT_OWNER,
|
||||
run: "You have to enter some code to execute first.",
|
||||
any: new RestCommand({
|
||||
// You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed.
|
||||
async run({send, message, channel, guild, author, member, client, args, combined}) {
|
||||
try {
|
||||
let evaled = eval(combined);
|
||||
if (typeof evaled !== "string") evaled = require("util").inspect(evaled);
|
||||
send(clean(evaled), {code: "js", split: true});
|
||||
} catch (err) {
|
||||
send(clean(err), {code: "js", split: true});
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
// TODO: Reimplement this entire command, for `send` doesn't allow
|
||||
// types like `unknown` to be sent anymore. Perhaps try to echo
|
||||
// whatever `evaled` is into an empty buffer and send this.
|
||||
// (see: `Buffer.alloc(...)`) This is unlikely to work though, since
|
||||
// `Buffer.alloc(...)` requires a length, which we can't retrieve from
|
||||
// an `unknown` variable.
|
||||
// eval: new NamedCommand({
|
||||
// description: "Evaluate code.",
|
||||
// usage: "<code>",
|
||||
// permission: PERMISSIONS.BOT_OWNER,
|
||||
// run: "You have to enter some code to execute first.",
|
||||
// any: new RestCommand({
|
||||
// // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed.
|
||||
// async run({send, message, channel, guild, author, member, client, args, combined}) {
|
||||
// try {
|
||||
// let evaled: unknown = eval(combined);
|
||||
// // If promises like message.channel.send() are invoked, await them so unnecessary error reports don't leak into the command handler.
|
||||
// // Also, it's more useful to see the value rather than Promise { <pending> }.
|
||||
// if (evaled instanceof Promise) evaled = await evaled;
|
||||
// if (typeof evaled !== "string") evaled = inspect(evaled);
|
||||
// // Also await this send call so that if the message is empty, it doesn't leak into the command handler.
|
||||
// await send(clean(evaled), {code: "js", split: true});
|
||||
// } catch (err) {
|
||||
// send(clean(err), {code: "js", split: true});
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }),
|
||||
nick: new NamedCommand({
|
||||
description: "Change the bot's nickname.",
|
||||
permission: PERMISSIONS.BOT_SUPPORT,
|
||||
channelType: CHANNEL_TYPE.GUILD,
|
||||
run: "You have to specify a nickname to set for the bot",
|
||||
any: new RestCommand({
|
||||
async run({send, message, guild, combined}) {
|
||||
async run({send, guild, combined}) {
|
||||
await guild!.me?.setNickname(combined);
|
||||
if (guild!.me?.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES)) message.delete({timeout: 5000});
|
||||
send(`Nickname set to \`${combined}\``).then((m) => m.delete({timeout: 5000}));
|
||||
send(`Nickname set to \`${combined}\``);
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
@ -321,8 +414,12 @@ export default new NamedCommand({
|
|||
description: "Shows a list of all guilds the bot is a member of.",
|
||||
permission: PERMISSIONS.BOT_SUPPORT,
|
||||
async run({send, client}) {
|
||||
const guildList = client.guilds.cache.array().map((e) => e.name);
|
||||
send(guildList, {split: true});
|
||||
const guildList = Util.splitMessage(
|
||||
Array.from(client.guilds.cache.map((e) => e.name).values()).join("\n")
|
||||
);
|
||||
for (let guildListPart of guildList) {
|
||||
send(guildListPart);
|
||||
}
|
||||
}
|
||||
}),
|
||||
activity: new NamedCommand({
|
||||
|
|
|
@ -22,7 +22,7 @@ export default new NamedCommand({
|
|||
const helpMenuPages: [string, string][] = []; // An array of (category, description) tuples.
|
||||
|
||||
// Prevent the description of one category from overflowing by splitting it into multiple pages if needed.
|
||||
for (const category of commands.keyArray()) {
|
||||
for (const category of commands.keys()) {
|
||||
const commandList = commands.get(category)!;
|
||||
let output = LEGEND;
|
||||
|
||||
|
@ -45,10 +45,16 @@ export default new NamedCommand({
|
|||
|
||||
paginate(send, author.id, helpMenuPages.length, (page, hasMultiplePages) => {
|
||||
const [category, output] = helpMenuPages[page];
|
||||
return new MessageEmbed()
|
||||
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${helpMenuPages.length})` : category)
|
||||
.setDescription(output)
|
||||
.setColor(EMBED_COLOR);
|
||||
return {
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setTitle(
|
||||
hasMultiplePages ? `${category} (Page ${page + 1} of ${helpMenuPages.length})` : category
|
||||
)
|
||||
.setDescription(output)
|
||||
.setColor(EMBED_COLOR)
|
||||
]
|
||||
};
|
||||
});
|
||||
},
|
||||
any: new RestCommand({
|
||||
|
@ -87,43 +93,45 @@ export default new NamedCommand({
|
|||
aliases = formattedAliases.join(", ") || "None";
|
||||
}
|
||||
|
||||
return send(
|
||||
new MessageEmbed()
|
||||
.setTitle(header)
|
||||
.setDescription(command.description)
|
||||
.setColor(EMBED_COLOR)
|
||||
.addFields(
|
||||
{
|
||||
name: "Aliases",
|
||||
value: aliases,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Category",
|
||||
value: category,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Permission Required",
|
||||
value: `\`${getPermissionName(result.permission)}\` (Level ${result.permission})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Channel Type",
|
||||
value: getChannelTypeName(result.channelType),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "NSFW Only?",
|
||||
value: result.nsfw ? "Yes" : "No",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Usages",
|
||||
value: append
|
||||
}
|
||||
)
|
||||
);
|
||||
return send({
|
||||
embeds: [
|
||||
new MessageEmbed()
|
||||
.setTitle(header)
|
||||
.setDescription(command.description)
|
||||
.setColor(EMBED_COLOR)
|
||||
.addFields(
|
||||
{
|
||||
name: "Aliases",
|
||||
value: aliases,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Category",
|
||||
value: category,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Permission Required",
|
||||
value: `\`${getPermissionName(result.permission)}\` (Level ${result.permission})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Channel Type",
|
||||
value: getChannelTypeName(result.channelType),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "NSFW Only?",
|
||||
value: result.nsfw ? "Yes" : "No",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Usages",
|
||||
value: append
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import {CHANNEL_TYPE, Command, NamedCommand} from "onion-lasers";
|
||||
import {registerWebhook, deleteWebhook} from "../../modules/webhookStorageManager";
|
||||
|
||||
// Because adding webhooks involves sending tokens, you'll want to prevent this from being used in non-private contexts.
|
||||
export default new NamedCommand({
|
||||
channelType: CHANNEL_TYPE.DM,
|
||||
description: "Manage webhooks stored by the bot.",
|
||||
usage: "register/delete <webhook URL>",
|
||||
run: "You need to use `register`/`delete`.",
|
||||
subcommands: {
|
||||
register: new NamedCommand({
|
||||
description: "Adds a webhook to the bot's storage.",
|
||||
any: new Command({
|
||||
async run({send, args}) {
|
||||
if (registerWebhook(args[0])) {
|
||||
send("Registered webhook with bot.");
|
||||
} else {
|
||||
send("Invalid webhook URL.");
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
delete: new NamedCommand({
|
||||
description: "Removes a webhook from the bot's storage.",
|
||||
any: new Command({
|
||||
async run({send, args}) {
|
||||
if (deleteWebhook(args[0])) {
|
||||
send("Deleted webhook.");
|
||||
} else send("Invalid webhook URL/ID.");
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
|
@ -1,24 +1,34 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import * as math from "mathjs";
|
||||
import {WolframClient} from "node-wolfram-alpha";
|
||||
import {MessageEmbed} from "discord.js";
|
||||
import {Config} from "../../structures";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Calculates a specified math expression.",
|
||||
run: "Please provide a calculation.",
|
||||
any: new RestCommand({
|
||||
async run({send, combined}) {
|
||||
if (Config.wolfram === null) return send("There's no Wolfram token in the config.");
|
||||
|
||||
const wClient = new WolframClient(Config.wolfram);
|
||||
let resp;
|
||||
try {
|
||||
resp = math.evaluate(combined);
|
||||
} catch (e) {
|
||||
return send("Please provide a *valid* calculation.");
|
||||
resp = await wClient.query(combined);
|
||||
} catch (e: any) {
|
||||
return send("Something went wrong.");
|
||||
}
|
||||
|
||||
if (!resp.data.queryresult.pods) return send("No pods were returned. Your query was likely invalid.");
|
||||
else {
|
||||
// TODO: Please don't hardcode the pod to fetch, try to figure out
|
||||
// which is the right one based on some comparisons instead
|
||||
const embed = new MessageEmbed()
|
||||
.setColor(0xffffff)
|
||||
.setTitle("Math Calculation")
|
||||
.addField("Input", `\`\`\`\n${combined}\`\`\``)
|
||||
.addField("Output", `\`\`\`\n${resp.data.queryresult.pods[1].subpods[0].plaintext}\`\`\``);
|
||||
return send({embeds: [embed]});
|
||||
}
|
||||
const embed = new MessageEmbed()
|
||||
.setColor(0xffffff)
|
||||
.setTitle("Math Calculation")
|
||||
.addField("Input", `\`\`\`js\n${combined}\`\`\``)
|
||||
.addField("Output", `\`\`\`js\n${resp}\`\`\``);
|
||||
return send(embed);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ export default new NamedCommand({
|
|||
const voiceChannel = message.member?.voice.channel;
|
||||
|
||||
if (!voiceChannel) return send("You are not in a voice channel.");
|
||||
if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS"))
|
||||
if (!voiceChannel.guild.me?.permissions.has("MANAGE_CHANNELS"))
|
||||
return send("I am lacking the required permissions to perform this action.");
|
||||
|
||||
const prevName = voiceChannel.name;
|
||||
|
|
|
@ -11,16 +11,16 @@ export default new NamedCommand({
|
|||
var queryString = args[0];
|
||||
let url = new URL(`https://djsdocs.sorta.moe/v2/embed?src=master&q=${queryString}`);
|
||||
const content = await getContent(url.toString());
|
||||
const msg = await send({embed: content});
|
||||
const msg = await send({embeds: [content]});
|
||||
const react = await msg.react("❌");
|
||||
|
||||
const collector = msg.createReactionCollector(
|
||||
(reaction, user) => {
|
||||
const collector = msg.createReactionCollector({
|
||||
filter: (reaction, user) => {
|
||||
if (user.id === author.id && reaction.emoji.name === "❌") msg.delete();
|
||||
return false;
|
||||
},
|
||||
{time: 60000}
|
||||
);
|
||||
time: 60000
|
||||
});
|
||||
|
||||
collector.on("end", () => {
|
||||
if (!msg.deleted) react.users.remove(msg.author);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import {processEmoteQueryFormatted} from "./modules/emote-utils";
|
||||
import {processEmoteQuery} from "./modules/emote-utils";
|
||||
|
||||
export default new NamedCommand({
|
||||
description:
|
||||
|
@ -9,7 +9,7 @@ export default new NamedCommand({
|
|||
description: "The emote(s) to send.",
|
||||
usage: "<emotes...>",
|
||||
async run({send, args}) {
|
||||
const output = processEmoteQueryFormatted(args);
|
||||
const output = processEmoteQuery(args, true).join("");
|
||||
if (output.length > 0) send(output);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import {MessageEmbed, version as djsversion, Guild, User, GuildMember} from "discord.js";
|
||||
import {MessageEmbed, version as djsversion, Guild, User, GuildMember, TextChannel, VoiceChannel} from "discord.js";
|
||||
import ms from "ms";
|
||||
import os from "os";
|
||||
import {Command, NamedCommand, getUserByNickname, CHANNEL_TYPE, getGuildByName, RestCommand} from "onion-lasers";
|
||||
import {formatBytes, trimArray} from "../../lib";
|
||||
import {verificationLevels, filterLevels, regions} from "../../defs/info";
|
||||
import {verificationLevels, filterLevels} from "../../defs/info";
|
||||
import moment, {utc} from "moment";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Command to provide all sorts of info about the current server, a user, etc.",
|
||||
async run({send, author, member}) {
|
||||
send(await getUserInfo(author, member));
|
||||
send({embeds: [await getUserInfo(author, member)]});
|
||||
},
|
||||
subcommands: {
|
||||
avatar: new NamedCommand({
|
||||
|
@ -55,39 +55,47 @@ export default new NamedCommand({
|
|||
const core = os.cpus()[0];
|
||||
const embed = new MessageEmbed()
|
||||
.setColor(guild?.me?.displayHexColor || "BLUE")
|
||||
.addField("General", [
|
||||
`**❯ Client:** ${client.user?.tag} (${client.user?.id})`,
|
||||
`**❯ Servers:** ${client.guilds.cache.size.toLocaleString()}`,
|
||||
`**❯ Users:** ${client.guilds.cache
|
||||
.reduce((a: any, b: {memberCount: any}) => a + b.memberCount, 0)
|
||||
.toLocaleString()}`,
|
||||
`**❯ Channels:** ${client.channels.cache.size.toLocaleString()}`,
|
||||
`**❯ Creation Date:** ${utc(client.user?.createdTimestamp).format("Do MMMM YYYY HH:mm:ss")}`,
|
||||
`**❯ Node.JS:** ${process.version}`,
|
||||
`**❯ Version:** v${process.env.npm_package_version}`,
|
||||
`**❯ Discord.JS:** ${djsversion}`,
|
||||
"\u200b"
|
||||
])
|
||||
.addField("System", [
|
||||
`**❯ Platform:** ${process.platform}`,
|
||||
`**❯ Uptime:** ${ms(os.uptime() * 1000, {
|
||||
long: true
|
||||
})}`,
|
||||
`**❯ CPU:**`,
|
||||
`\u3000 • Cores: ${os.cpus().length}`,
|
||||
`\u3000 • Model: ${core.model}`,
|
||||
`\u3000 • Speed: ${core.speed}MHz`,
|
||||
`**❯ Memory:**`,
|
||||
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`,
|
||||
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}`
|
||||
])
|
||||
.addField(
|
||||
"General",
|
||||
[
|
||||
`**❯ Client:** ${client.user?.tag} (${client.user?.id})`,
|
||||
`**❯ Servers:** ${client.guilds.cache.size.toLocaleString()}`,
|
||||
`**❯ Users:** ${client.guilds.cache
|
||||
.reduce((a: any, b: {memberCount: any}) => a + b.memberCount, 0)
|
||||
.toLocaleString()}`,
|
||||
`**❯ Channels:** ${client.channels.cache.size.toLocaleString()}`,
|
||||
`**❯ Creation Date:** ${utc(client.user?.createdTimestamp).format(
|
||||
"Do MMMM YYYY HH:mm:ss"
|
||||
)}`,
|
||||
`**❯ Node.JS:** ${process.version}`,
|
||||
`**❯ Version:** v${process.env.npm_package_version}`,
|
||||
`**❯ Discord.JS:** v${djsversion}`,
|
||||
"\u200b"
|
||||
].join("\n")
|
||||
)
|
||||
.addField(
|
||||
"System",
|
||||
[
|
||||
`**❯ Platform:** ${process.platform}`,
|
||||
`**❯ Uptime:** ${ms(os.uptime() * 1000, {
|
||||
long: true
|
||||
})}`,
|
||||
`**❯ CPU:**`,
|
||||
`\u3000 • Cores: ${os.cpus().length}`,
|
||||
`\u3000 • Model: ${core.model}`,
|
||||
`\u3000 • Speed: ${core.speed}MHz`,
|
||||
`**❯ Memory:**`,
|
||||
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`,
|
||||
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}`
|
||||
].join("\n")
|
||||
)
|
||||
.setTimestamp();
|
||||
const avatarURL = client.user?.displayAvatarURL({
|
||||
dynamic: true,
|
||||
size: 2048
|
||||
});
|
||||
if (avatarURL) embed.setThumbnail(avatarURL);
|
||||
send(embed);
|
||||
send({embeds: [embed]});
|
||||
}
|
||||
}),
|
||||
guild: new NamedCommand({
|
||||
|
@ -95,14 +103,14 @@ export default new NamedCommand({
|
|||
usage: "(<guild name>/<guild ID>)",
|
||||
channelType: CHANNEL_TYPE.GUILD,
|
||||
async run({send, guild}) {
|
||||
send(await getGuildInfo(guild!, guild));
|
||||
send({embeds: [await getGuildInfo(guild!, guild)]});
|
||||
},
|
||||
id: "guild",
|
||||
guild: new Command({
|
||||
description: "Display info about a guild by its ID.",
|
||||
async run({send, guild, args}) {
|
||||
const targetGuild = args[0] as Guild;
|
||||
send(await getGuildInfo(targetGuild, guild));
|
||||
send({embeds: [await getGuildInfo(targetGuild, guild)]});
|
||||
}
|
||||
}),
|
||||
any: new RestCommand({
|
||||
|
@ -111,7 +119,7 @@ export default new NamedCommand({
|
|||
const targetGuild = getGuildByName(combined);
|
||||
|
||||
if (typeof targetGuild !== "string") {
|
||||
send(await getGuildInfo(targetGuild, guild));
|
||||
send({embeds: [await getGuildInfo(targetGuild, guild)]});
|
||||
} else {
|
||||
send(targetGuild);
|
||||
}
|
||||
|
@ -126,7 +134,7 @@ export default new NamedCommand({
|
|||
const user = args[0] as User;
|
||||
// Transforms the User object into a GuildMember object of the current guild.
|
||||
const member = guild?.members.resolve(user);
|
||||
send(await getUserInfo(user, member));
|
||||
send({embeds: [await getUserInfo(user, member)]});
|
||||
}
|
||||
}),
|
||||
any: new RestCommand({
|
||||
|
@ -135,7 +143,7 @@ export default new NamedCommand({
|
|||
const user = await getUserByNickname(combined, guild);
|
||||
// Transforms the User object into a GuildMember object of the current guild.
|
||||
const member = guild?.members.resolve(user);
|
||||
if (typeof user !== "string") send(await getUserInfo(user, member));
|
||||
if (typeof user !== "string") send({embeds: [await getUserInfo(user, member)]});
|
||||
else send(user);
|
||||
}
|
||||
})
|
||||
|
@ -147,20 +155,21 @@ async function getUserInfo(user: User, member: GuildMember | null | undefined):
|
|||
const embed = new MessageEmbed()
|
||||
.setThumbnail(user.displayAvatarURL({dynamic: true, size: 512}))
|
||||
.setColor("BLUE")
|
||||
.addField("User", [
|
||||
`**❯ Username:** ${user.username}`,
|
||||
`**❯ Discriminator:** ${user.discriminator}`,
|
||||
`**❯ ID:** ${user.id}`,
|
||||
`**❯ Flags:** ${userFlags.length ? userFlags.join(", ") : "None"}`,
|
||||
`**❯ Avatar:** [Link to avatar](${user.displayAvatarURL({
|
||||
dynamic: true
|
||||
})})`,
|
||||
`**❯ Time Created:** ${moment(user.createdTimestamp).format("LT")} ${moment(user.createdTimestamp).format(
|
||||
"LL"
|
||||
)} ${moment(user.createdTimestamp).fromNow()}`,
|
||||
`**❯ Status:** ${user.presence.status}`,
|
||||
`**❯ Game:** ${user.presence.activities || "Not playing a game."}`
|
||||
]);
|
||||
.addField(
|
||||
"User",
|
||||
[
|
||||
`**❯ Username:** ${user.username}`,
|
||||
`**❯ Discriminator:** ${user.discriminator}`,
|
||||
`**❯ ID:** ${user.id}`,
|
||||
`**❯ Flags:** ${userFlags.length ? userFlags.join(", ") : "None"}`,
|
||||
`**❯ Avatar:** [Link to avatar](${user.displayAvatarURL({
|
||||
dynamic: true
|
||||
})})`,
|
||||
`**❯ Time Created:** ${moment(user.createdTimestamp).format("LT")} ${moment(
|
||||
user.createdTimestamp
|
||||
).format("LL")} ${moment(user.createdTimestamp).fromNow()}`
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
if (member) {
|
||||
const roles = member.roles.cache
|
||||
|
@ -170,16 +179,21 @@ async function getUserInfo(user: User, member: GuildMember | null | undefined):
|
|||
|
||||
embed
|
||||
.setColor(member.displayHexColor)
|
||||
.addField("Member", [
|
||||
`**❯ Highest Role:** ${
|
||||
member.roles.highest.id === member.guild.id ? "None" : member.roles.highest.name
|
||||
}`,
|
||||
`**❯ Server Join Date:** ${moment(member.joinedAt).format("LL LTS")}`,
|
||||
`**❯ Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : "None"}`,
|
||||
`**❯ Roles:** [${roles.length}]: ${
|
||||
roles.length == 0 ? "None" : roles.length <= 10 ? roles.join(", ") : trimArray(roles).join(", ")
|
||||
}`
|
||||
]);
|
||||
.addField(
|
||||
"Member",
|
||||
[
|
||||
`**❯ Status:** ${member.presence?.status}`,
|
||||
`**❯ Game:** ${member.presence?.activities ?? "Not playing a game."}`,
|
||||
`**❯ Highest Role:** ${
|
||||
member.roles.highest.id === member.guild.id ? "None" : member.roles.highest.name
|
||||
}`,
|
||||
`**❯ Server Join Date:** ${moment(member.joinedAt).format("LL LTS")}`,
|
||||
`**❯ Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : "None"}`,
|
||||
`**❯ Roles:** [${roles.length}]: ${
|
||||
roles.length == 0 ? "None" : roles.length <= 10 ? roles.join(", ") : trimArray(roles).join(", ")
|
||||
}`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
return embed;
|
||||
|
@ -196,14 +210,15 @@ async function getGuildInfo(guild: Guild, currentGuild: Guild | null) {
|
|||
const iconURL = guild.iconURL({dynamic: true});
|
||||
const embed = new MessageEmbed().setDescription(`**Guild information for __${guild.name}__**`).setColor("BLUE");
|
||||
const displayRoles = !!(currentGuild && guild.id === currentGuild.id);
|
||||
if (iconURL) {
|
||||
embed
|
||||
.setThumbnail(iconURL)
|
||||
.addField("General", [
|
||||
const owner = await guild.fetchOwner();
|
||||
|
||||
embed
|
||||
.addField(
|
||||
"General",
|
||||
[
|
||||
`**❯ Name:** ${guild.name}`,
|
||||
`**❯ ID:** ${guild.id}`,
|
||||
`**❯ Owner:** ${guild.owner?.user.tag} (${guild.ownerID})`,
|
||||
`**❯ Region:** ${regions[guild.region]}`,
|
||||
`**❯ Owner:** ${owner.user.tag} (${guild.ownerId})`,
|
||||
`**❯ Boost Tier:** ${guild.premiumTier ? `Tier ${guild.premiumTier}` : "None"}`,
|
||||
`**❯ Explicit Filter:** ${filterLevels[guild.explicitContentFilter]}`,
|
||||
`**❯ Verification Level:** ${verificationLevels[guild.verificationLevel]}`,
|
||||
|
@ -211,36 +226,44 @@ async function getGuildInfo(guild: Guild, currentGuild: Guild | null) {
|
|||
guild.createdTimestamp
|
||||
).format("LL")} ${moment(guild.createdTimestamp).fromNow()}`,
|
||||
"\u200b"
|
||||
])
|
||||
.addField("Statistics", [
|
||||
].join("\n")
|
||||
)
|
||||
.addField(
|
||||
"Statistics",
|
||||
[
|
||||
`**❯ Role Count:** ${roles.length}`,
|
||||
`**❯ Emoji Count:** ${emojis.size}`,
|
||||
`**❯ Regular Emoji Count:** ${emojis.filter((emoji) => !emoji.animated).size}`,
|
||||
`**❯ Animated Emoji Count:** ${emojis.filter((emoji) => emoji.animated).size}`,
|
||||
`**❯ Animated Emoji Count:** ${emojis.filter((emoji) => !!emoji.animated).size}`,
|
||||
`**❯ Member Count:** ${guild.memberCount}`,
|
||||
`**❯ Humans:** ${members.filter((member) => !member.user.bot).size}`,
|
||||
`**❯ Bots:** ${members.filter((member) => member.user.bot).size}`,
|
||||
`**❯ Text Channels:** ${channels.filter((channel) => channel.type === "text").size}`,
|
||||
`**❯ Voice Channels:** ${channels.filter((channel) => channel.type === "voice").size}`,
|
||||
`**❯ Text Channels:** ${channels.filter((channel) => channel instanceof TextChannel).size}`,
|
||||
`**❯ Voice Channels:** ${channels.filter((channel) => channel instanceof VoiceChannel).size}`,
|
||||
`**❯ Boost Count:** ${guild.premiumSubscriptionCount || "0"}`,
|
||||
`\u200b`
|
||||
])
|
||||
.addField("Presence", [
|
||||
`**❯ Online:** ${members.filter((member) => member.presence.status === "online").size}`,
|
||||
`**❯ Idle:** ${members.filter((member) => member.presence.status === "idle").size}`,
|
||||
`**❯ Do Not Disturb:** ${members.filter((member) => member.presence.status === "dnd").size}`,
|
||||
`**❯ Offline:** ${members.filter((member) => member.presence.status === "offline").size}`,
|
||||
].join("\n")
|
||||
)
|
||||
.addField(
|
||||
"Presence",
|
||||
[
|
||||
`**❯ Online:** ${members.filter((member) => member.presence?.status === "online").size}`,
|
||||
`**❯ Idle:** ${members.filter((member) => member.presence?.status === "idle").size}`,
|
||||
`**❯ Do Not Disturb:** ${members.filter((member) => member.presence?.status === "dnd").size}`,
|
||||
`**❯ Offline:** ${members.filter((member) => member.presence?.status === "offline").size}`,
|
||||
displayRoles ? "\u200b" : ""
|
||||
])
|
||||
.setTimestamp();
|
||||
].join("\n")
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
// Only add the roles if the guild the bot is sending the message to is the same one that's being requested.
|
||||
if (displayRoles) {
|
||||
embed.addField(
|
||||
`Roles [${roles.length - 1}]`,
|
||||
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles) : "None"
|
||||
);
|
||||
}
|
||||
if (iconURL) embed.setThumbnail(iconURL);
|
||||
|
||||
// Only add the roles if the guild the bot is sending the message to is the same one that's being requested.
|
||||
if (displayRoles) {
|
||||
embed.addField(
|
||||
`Roles [${roles.length - 1}]`,
|
||||
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles).join(", ") : "None"
|
||||
);
|
||||
}
|
||||
|
||||
return embed;
|
||||
|
|
|
@ -3,7 +3,11 @@ import {Command, NamedCommand} from "onion-lasers";
|
|||
export default new NamedCommand({
|
||||
description: "Gives you the invite link.",
|
||||
async run({send, client}) {
|
||||
send(`https://discordapp.com/api/oauth2/authorize?client_id=${client.user!.id}&permissions=8&scope=bot`);
|
||||
send(
|
||||
`https://discordapp.com/api/oauth2/authorize?client_id=${
|
||||
client.user!.id
|
||||
}&permissions=138046467152&scope=bot`
|
||||
);
|
||||
},
|
||||
number: new Command({
|
||||
async run({send, client, args}) {
|
||||
|
|
|
@ -9,7 +9,7 @@ export default new NamedCommand({
|
|||
description: "Lists all emotes the bot has in it's registry,",
|
||||
usage: "<regex pattern> (-flags)",
|
||||
async run({send, author, client}) {
|
||||
displayEmoteList(client.emojis.cache.array(), send, author);
|
||||
displayEmoteList(Array.from(client.emojis.cache.values()), send, author);
|
||||
},
|
||||
any: new RestCommand({
|
||||
description:
|
||||
|
@ -20,7 +20,7 @@ export default new NamedCommand({
|
|||
const guildID: string = args[0];
|
||||
|
||||
displayEmoteList(
|
||||
client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(),
|
||||
Array.from(client.emojis.cache.filter((emote) => emote.guild.id === guildID).values()),
|
||||
send,
|
||||
author
|
||||
);
|
||||
|
@ -32,13 +32,15 @@ export default new NamedCommand({
|
|||
flags = args.pop().substring(1);
|
||||
}
|
||||
|
||||
let emoteCollection = client.emojis.cache.array();
|
||||
let emoteCollection = Array.from(client.emojis.cache.values());
|
||||
// Creates a sandbox to stop a regular expression if it takes too much time to search.
|
||||
// To avoid passing in a giant data structure, I'll just pass in the structure {[id: string]: [name: string]}.
|
||||
let emotes = new Map<string, string>();
|
||||
|
||||
for (const emote of emoteCollection) {
|
||||
emotes.set(emote.id, emote.name);
|
||||
if (emote.name) {
|
||||
emotes.set(emote.id, emote.name);
|
||||
}
|
||||
}
|
||||
|
||||
// The result will be sandbox.emotes because it'll be modified in-place.
|
||||
|
@ -59,12 +61,18 @@ export default new NamedCommand({
|
|||
emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted.
|
||||
displayEmoteList(emoteCollection, send, author);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
||||
// FIXME: `error` is of type `unknown` here.
|
||||
// Also: <https://stackoverflow.com/questions/40141005/property-code-does-not-exist-on-type-error>
|
||||
let errorName = "???";
|
||||
if (error instanceof Error) {
|
||||
errorName = error.name;
|
||||
}
|
||||
if (errorName === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
||||
send(
|
||||
`The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.`
|
||||
);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
throw new Error(errorName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -77,6 +85,7 @@ export default new NamedCommand({
|
|||
|
||||
async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author: User) {
|
||||
emotes.sort((a, b) => {
|
||||
if (!a.name || !b.name) return 0;
|
||||
const first = a.name.toLowerCase();
|
||||
const second = b.name.toLowerCase();
|
||||
|
||||
|
@ -99,7 +108,7 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author
|
|||
}
|
||||
embed.setDescription(desc);
|
||||
|
||||
return embed;
|
||||
return {embeds: [embed]};
|
||||
});
|
||||
} else {
|
||||
send("No valid emotes found by that query.");
|
||||
|
|
|
@ -51,9 +51,11 @@ function searchSimilarEmotes(query: string): GuildEmoji[] {
|
|||
const emoteCandidates: {emote: GuildEmoji; dist: number}[] = [];
|
||||
|
||||
for (const emote of client.emojis.cache.values()) {
|
||||
const dist = levenshtein(emote.name, query);
|
||||
if (dist <= maxAcceptedDistance) {
|
||||
emoteCandidates.push({emote, dist});
|
||||
if (emote.name) {
|
||||
const dist = levenshtein(emote.name, query);
|
||||
if (dist <= maxAcceptedDistance) {
|
||||
emoteCandidates.push({emote, dist});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,11 +63,40 @@ function searchSimilarEmotes(query: string): GuildEmoji[] {
|
|||
return emoteCandidates.map((em) => em.emote);
|
||||
}
|
||||
|
||||
const unicodeEmojiRegex = /^(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])[\ufe00-\ufe0f]?$/;
|
||||
const unicodeEmojiRegex =
|
||||
/^(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])[\ufe00-\ufe0f]?$/;
|
||||
const discordEmoteMentionRegex = /^<a?:\w+:\d+>$/;
|
||||
const emoteNameWithSelectorRegex = /^(.+)~(\d+)$/;
|
||||
|
||||
function processEmoteQuery(query: string[], isFormatted: boolean): string[] {
|
||||
export function searchNearestEmote(query: string, additionalEmotes?: GuildEmoji[]): string {
|
||||
// Selector number used for disambiguating multiple emotes with same name.
|
||||
let selector = 0;
|
||||
|
||||
// If the query has emoteName~123 format, extract the actual name and the selector number.
|
||||
const queryWithSelector = query.match(emoteNameWithSelectorRegex);
|
||||
if (queryWithSelector) {
|
||||
query = queryWithSelector[1];
|
||||
selector = +queryWithSelector[2];
|
||||
}
|
||||
|
||||
// Try to match an emote name directly if the selector is for the closest match.
|
||||
if (selector == 0) {
|
||||
const directMatchEmote = client.emojis.cache.find((em) => em.name === query);
|
||||
if (directMatchEmote) return directMatchEmote.toString();
|
||||
}
|
||||
|
||||
// Find all similar emote candidates within certain threshold and select Nth top one according to the selector.
|
||||
const similarEmotes = searchSimilarEmotes(query);
|
||||
if (similarEmotes.length > 0) {
|
||||
selector = Math.min(selector, similarEmotes.length - 1);
|
||||
return similarEmotes[selector].toString();
|
||||
}
|
||||
|
||||
// Return some "missing/invalid emote" indicator.
|
||||
return "❓";
|
||||
}
|
||||
|
||||
export function processEmoteQuery(query: string[], isFormatted: boolean): string[] {
|
||||
return query.map((emote) => {
|
||||
emote = emote.trim();
|
||||
|
||||
|
@ -79,33 +110,6 @@ function processEmoteQuery(query: string[], isFormatted: boolean): string[] {
|
|||
if (emote == "_") return "\u200b";
|
||||
}
|
||||
|
||||
// Selector number used for disambiguating multiple emotes with same name.
|
||||
let selector = 0;
|
||||
|
||||
// If the query has emoteName~123 format, extract the actual name and the selector number.
|
||||
const queryWithSelector = emote.match(emoteNameWithSelectorRegex);
|
||||
if (queryWithSelector) {
|
||||
emote = queryWithSelector[1];
|
||||
selector = +queryWithSelector[2];
|
||||
}
|
||||
|
||||
// Try to match an emote name directly if the selector is for the closest match.
|
||||
if (selector == 0) {
|
||||
const directMatchEmote = client.emojis.cache.find((em) => em.name === emote);
|
||||
if (directMatchEmote) return directMatchEmote.toString();
|
||||
}
|
||||
|
||||
// Find all similar emote candidates within certian threshold and select Nth top one according to the selector.
|
||||
const similarEmotes = searchSimilarEmotes(emote);
|
||||
if (similarEmotes.length > 0) {
|
||||
selector = Math.min(selector, similarEmotes.length - 1);
|
||||
return similarEmotes[selector].toString();
|
||||
}
|
||||
|
||||
// Return some "missing/invalid emote" indicator.
|
||||
return "❓";
|
||||
return searchNearestEmote(emote);
|
||||
});
|
||||
}
|
||||
|
||||
export const processEmoteQueryArray = (query: string[]): string[] => processEmoteQuery(query, false);
|
||||
export const processEmoteQueryFormatted = (query: string[]): string => processEmoteQuery(query, true).join("");
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import {NamedCommand, getPermissionLevel, getPermissionName, hasPermission} from "onion-lasers";
|
||||
import {DMChannel, Permissions} from "discord.js";
|
||||
|
||||
export default new NamedCommand({
|
||||
description:
|
||||
"Purges the bot's messages in either a guild channel (requiring the BOT_SUPPORT permission level) or a DM channel (no permission required). Limited to the last 100 messages.",
|
||||
async run({send, message, channel, guild, client, author, member}) {
|
||||
if (channel instanceof DMChannel) {
|
||||
const messages = await channel.messages.fetch({
|
||||
limit: 100
|
||||
});
|
||||
|
||||
for (const message of messages.values()) {
|
||||
if (message.author.id === client.user!.id) {
|
||||
message.delete();
|
||||
}
|
||||
}
|
||||
} else if (hasPermission(author, member, PERMISSIONS.BOT_SUPPORT)) {
|
||||
if (guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES)) message.delete();
|
||||
|
||||
const messages = await channel.messages.fetch({
|
||||
limit: 100
|
||||
});
|
||||
const travMessages = messages.filter((msg) => msg.author.id === client.user!.id);
|
||||
|
||||
send(`Found ${travMessages.size} messages to delete.`).then((msg) => setTimeout(() => msg.delete(), 5000));
|
||||
|
||||
// It's better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES.
|
||||
for (const message of messages.values()) {
|
||||
if (message.author.id === client.user!.id) {
|
||||
message.delete();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const userPermLevel = getPermissionLevel(author, member);
|
||||
send(
|
||||
`You don't have access to this command! Your permission level is \`${getPermissionName(
|
||||
userPermLevel
|
||||
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
|
||||
PERMISSIONS.BOT_SUPPORT
|
||||
)}\` (${PERMISSIONS.BOT_SUPPORT}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,8 +1,9 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import {Message, Channel, TextChannel} from "discord.js";
|
||||
import {processEmoteQueryArray} from "./modules/emote-utils";
|
||||
import {Message, Channel, TextChannel, TextBasedChannel} from "discord.js";
|
||||
import {processEmoteQuery} from "./modules/emote-utils";
|
||||
|
||||
export default new NamedCommand({
|
||||
aliases: ["r"],
|
||||
description:
|
||||
"Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.",
|
||||
usage: 'react <emotes...> (<distance / message ID / "Copy ID" / "Copy Message Link">)',
|
||||
|
@ -14,7 +15,7 @@ export default new NamedCommand({
|
|||
|
||||
if (message.reference) {
|
||||
// If the command message is a reply to another message, use that as the react target.
|
||||
target = await channel.messages.fetch(message.reference.messageID!);
|
||||
target = await channel.messages.fetch(message.reference.messageId!);
|
||||
}
|
||||
// handles reacts by message id/distance
|
||||
else if (args.length >= 2) {
|
||||
|
@ -28,7 +29,7 @@ export default new NamedCommand({
|
|||
const guildID = match[1];
|
||||
const channelID = match[2];
|
||||
const messageID = match[3];
|
||||
let tmpChannel: Channel | undefined = channel;
|
||||
let tmpChannel: TextBasedChannel | undefined = channel;
|
||||
|
||||
if (guild?.id !== guildID) {
|
||||
try {
|
||||
|
@ -38,12 +39,13 @@ export default new NamedCommand({
|
|||
}
|
||||
}
|
||||
|
||||
if (tmpChannel.id !== channelID) tmpChannel = guild.channels.cache.get(channelID);
|
||||
if (tmpChannel?.id !== channelID)
|
||||
tmpChannel = guild.channels.cache.get(channelID) as TextBasedChannel;
|
||||
if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`);
|
||||
|
||||
if (message.id !== messageID) {
|
||||
try {
|
||||
target = await (tmpChannel as TextChannel).messages.fetch(messageID);
|
||||
target = await tmpChannel.messages.fetch(messageID);
|
||||
} catch {
|
||||
return send(`\`${messageID}\` is an invalid message ID!`);
|
||||
}
|
||||
|
@ -56,14 +58,15 @@ export default new NamedCommand({
|
|||
const match = copyIDPattern.exec(last)!;
|
||||
const channelID = match[1];
|
||||
const messageID = match[2];
|
||||
let tmpChannel: Channel | undefined = channel;
|
||||
let tmpChannel: TextBasedChannel | undefined = channel;
|
||||
|
||||
if (tmpChannel.id !== channelID) tmpChannel = guild?.channels.cache.get(channelID);
|
||||
if (tmpChannel?.id !== channelID)
|
||||
tmpChannel = guild?.channels.cache.get(channelID) as TextBasedChannel;
|
||||
if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`);
|
||||
|
||||
if (message.id !== messageID) {
|
||||
try {
|
||||
target = await (tmpChannel as TextChannel).messages.fetch(messageID);
|
||||
target = await tmpChannel.messages.fetch(messageID);
|
||||
} catch {
|
||||
return send(`\`${messageID}\` is an invalid message ID!`);
|
||||
}
|
||||
|
@ -100,7 +103,7 @@ export default new NamedCommand({
|
|||
).last();
|
||||
}
|
||||
|
||||
for (const emote of processEmoteQueryArray(args)) {
|
||||
for (const emote of processEmoteQuery(args, false)) {
|
||||
// Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want
|
||||
const reaction = await target!.react(emote);
|
||||
|
||||
|
|
|
@ -1,13 +1,75 @@
|
|||
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||
import {NamedCommand, RestCommand, CHANNEL_TYPE} from "onion-lasers";
|
||||
import {TextChannel, NewsChannel, Permissions} from "discord.js";
|
||||
import {searchNearestEmote} from "../utility/modules/emote-utils";
|
||||
import {resolveWebhook} from "../../modules/webhookStorageManager";
|
||||
import {parseVarsCallback} from "../../lib";
|
||||
|
||||
// Description //
|
||||
// This is the message-based counterpart to the react command, which replicates Nitro's ability to send emotes in messages.
|
||||
// This takes advantage of webhooks' ability to change the username and avatar per request.
|
||||
// Uses "@user says:" as a fallback in case no webhook is set for the channel.
|
||||
|
||||
// Limitations / Points of Interest //
|
||||
// - Webhooks can fetch any emote in existence and use it as long as it hasn't been deleted.
|
||||
// - The emote name from <:name:id> DOES matter if the user isn't part of that guild. That's the fallback essentially, otherwise, it doesn't matter.
|
||||
// - The animated flag must be correct. <:name:id> on an animated emote will make it not animated, <a:name:id> will display an invalid image.
|
||||
// - Rate limits for webhooks shouldn't be that big of an issue (5 requests every 2 seconds).
|
||||
export default new NamedCommand({
|
||||
description: "Repeats your message.",
|
||||
aliases: ["s"],
|
||||
channelType: CHANNEL_TYPE.GUILD,
|
||||
description: "Repeats your message with emotes in /slashes/.",
|
||||
usage: "<message>",
|
||||
run: "Please provide a message for me to say!",
|
||||
any: new RestCommand({
|
||||
description: "Message to repeat.",
|
||||
async run({send, author, combined}) {
|
||||
send(`*${author} says:*\n${combined}`);
|
||||
async run({send, channel, author, member, message, combined, guild}) {
|
||||
const webhook = await resolveWebhook(channel as TextChannel | NewsChannel);
|
||||
|
||||
if (webhook) {
|
||||
const resolvedMessage = resolveMessageWithEmotes(combined);
|
||||
|
||||
if (resolvedMessage)
|
||||
webhook.send({
|
||||
content: resolvedMessage,
|
||||
username: member!.nickname ?? author.username,
|
||||
// Webhooks cannot have animated avatars, so requesting the animated version is a moot point.
|
||||
avatarURL:
|
||||
author.avatarURL({
|
||||
format: "png"
|
||||
}) || author.defaultAvatarURL,
|
||||
allowedMentions: {parse: []}, // avoids double pings
|
||||
// "embeds" will not be included because it messes with the default ones that generate
|
||||
files: Array.from(message.attachments.values())
|
||||
});
|
||||
else send("Cannot send an empty message.");
|
||||
} else {
|
||||
const resolvedMessage = resolveMessageWithEmotes(combined);
|
||||
if (resolvedMessage)
|
||||
send({content: `*${author} says:*\n${resolvedMessage}`, allowedMentions: {parse: []}});
|
||||
else send("Cannot send an empty message.");
|
||||
}
|
||||
|
||||
if (guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES)) message.delete();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const FETCH_EMOTE_PATTERN = /^(\d{17,})(?: ([^ ]+?))?(?: (a))?$/;
|
||||
|
||||
// Send extra emotes only for webhook messages (because the bot user can't fetch any emote in existence while webhooks can).
|
||||
function resolveMessageWithEmotes(text: string, extraEmotes?: null): string {
|
||||
return parseVarsCallback(
|
||||
text,
|
||||
(variable) => {
|
||||
if (FETCH_EMOTE_PATTERN.test(variable)) {
|
||||
// Although I *could* make this ping the CDN to see if gif exists to see whether it's animated or not, it'd take too much time to wait on it.
|
||||
// Plus, with the way this function is setup, I wouldn't be able to incorporate a search without changing the function to async.
|
||||
const [_, id, name, animated] = FETCH_EMOTE_PATTERN.exec(variable)!;
|
||||
return `<${animated ?? ""}:${name ?? "_"}:${id}>`;
|
||||
}
|
||||
|
||||
return searchNearestEmote(variable);
|
||||
},
|
||||
"/"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {NamedCommand, CHANNEL_TYPE} from "onion-lasers";
|
||||
import {pluralise} from "../../lib";
|
||||
import moment from "moment";
|
||||
import {Collection, TextChannel} from "discord.js";
|
||||
import {Collection, TextChannel, Util} from "discord.js";
|
||||
|
||||
const lastUsedTimestamps = new Collection<string, number>();
|
||||
|
||||
|
@ -24,7 +24,7 @@ export default new NamedCommand({
|
|||
|
||||
const stats: {
|
||||
[id: string]: {
|
||||
name: string;
|
||||
name: string | null;
|
||||
formatted: string;
|
||||
users: number;
|
||||
bots: number;
|
||||
|
@ -33,7 +33,7 @@ export default new NamedCommand({
|
|||
let totalUserEmoteUsage = 0;
|
||||
// IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise.
|
||||
const allTextChannelsInCurrentGuild = guild!.channels.cache.filter(
|
||||
(channel) => channel.type === "text" && channel.viewable
|
||||
(channel) => channel instanceof TextChannel && channel.viewable
|
||||
) as Collection<string, TextChannel>;
|
||||
let messagesSearched = 0;
|
||||
let channelsSearched = 0;
|
||||
|
@ -41,7 +41,7 @@ export default new NamedCommand({
|
|||
const totalChannels = allTextChannelsInCurrentGuild.size;
|
||||
const statusMessage = await send("Gathering emotes...");
|
||||
let warnings = 0;
|
||||
channel.startTyping();
|
||||
channel.sendTyping();
|
||||
|
||||
// Initialize the emote stats object with every emote in the current guild.
|
||||
// The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with.
|
||||
|
@ -63,7 +63,7 @@ export default new NamedCommand({
|
|||
|
||||
for (const channel of allTextChannelsInCurrentGuild.values()) {
|
||||
currentChannelName = channel.name;
|
||||
let selected = channel.lastMessageID ?? message.id;
|
||||
let selected = channel.lastMessageId ?? message.id;
|
||||
let continueLoop = true;
|
||||
|
||||
while (continueLoop) {
|
||||
|
@ -129,6 +129,7 @@ export default new NamedCommand({
|
|||
|
||||
if (reaction.count !== userReactions + botReactions) {
|
||||
console.warn(
|
||||
"[scanemotes]",
|
||||
`[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`
|
||||
);
|
||||
warnings++;
|
||||
|
@ -159,8 +160,7 @@ export default new NamedCommand({
|
|||
"y"
|
||||
)}.`
|
||||
);
|
||||
console.log(`Finished operation in ${finishTime - startTime} ms.`);
|
||||
channel.stopTyping();
|
||||
console.log("[scanemotes]", `Finished operation in ${finishTime - startTime} ms.`);
|
||||
|
||||
// Display stats on emote usage.
|
||||
// This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end.
|
||||
|
@ -179,7 +179,10 @@ export default new NamedCommand({
|
|||
);
|
||||
}
|
||||
|
||||
return await send(lines, {split: true});
|
||||
let emoteList = Util.splitMessage(lines.join("\n"));
|
||||
for (let emoteListPart of emoteList) {
|
||||
return await send(emoteListPart);
|
||||
}
|
||||
},
|
||||
subcommands: {
|
||||
forcereset: new NamedCommand({
|
||||
|
|
|
@ -34,11 +34,14 @@ export default new NamedCommand({
|
|||
const stream = streamList.get(userID)!;
|
||||
stream.description = combined;
|
||||
stream.update();
|
||||
send("Successfully set the stream description to:", {
|
||||
embed: {
|
||||
description: stream.description,
|
||||
color: member!.displayColor
|
||||
}
|
||||
send({
|
||||
content: "Successfully set the stream description to:",
|
||||
embeds: [
|
||||
{
|
||||
description: stream.description,
|
||||
color: member!.displayColor
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
|
@ -70,12 +73,15 @@ export default new NamedCommand({
|
|||
const stream = streamList.get(userID)!;
|
||||
stream.thumbnail = combined;
|
||||
stream.update();
|
||||
send(`Successfully set the stream thumbnail to: ${combined}`, {
|
||||
embed: {
|
||||
description: stream.description,
|
||||
thumbnail: {url: combined},
|
||||
color: member!.displayColor
|
||||
}
|
||||
send({
|
||||
content: `Successfully set the stream thumbnail to: ${combined}`,
|
||||
embeds: [
|
||||
{
|
||||
description: stream.description,
|
||||
thumbnail: {url: combined},
|
||||
color: member!.displayColor
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
|
|
|
@ -124,42 +124,40 @@ function getTimeEmbed(user: User) {
|
|||
}
|
||||
|
||||
const embed = {
|
||||
embed: {
|
||||
color: TIME_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.username,
|
||||
icon_url: user.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
color: TIME_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.username,
|
||||
icon_url: user.displayAvatarURL({
|
||||
format: "png",
|
||||
dynamic: true
|
||||
})
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Local Date",
|
||||
value: localDate
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Local Date",
|
||||
value: localDate
|
||||
},
|
||||
{
|
||||
name: "Day of the Week",
|
||||
value: dayOfWeek
|
||||
},
|
||||
{
|
||||
name: "Local Time",
|
||||
value: localTime
|
||||
},
|
||||
{
|
||||
name: daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset",
|
||||
value: timezoneOffset
|
||||
},
|
||||
{
|
||||
name: "Observes Daylight Savings?",
|
||||
value: daylightSavingsRegion ? "Yes" : "No"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
name: "Day of the Week",
|
||||
value: dayOfWeek
|
||||
},
|
||||
{
|
||||
name: "Local Time",
|
||||
value: localTime
|
||||
},
|
||||
{
|
||||
name: daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset",
|
||||
value: timezoneOffset
|
||||
},
|
||||
{
|
||||
name: "Observes Daylight Savings?",
|
||||
value: daylightSavingsRegion ? "Yes" : "No"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (daylightSavingsRegion) {
|
||||
embed.embed.fields.push(
|
||||
embed.fields.push(
|
||||
{
|
||||
name: "Daylight Savings Active?",
|
||||
value: hasDaylightSavings(daylightSavingsRegion) ? "Yes" : "No"
|
||||
|
@ -178,7 +176,7 @@ export default new NamedCommand({
|
|||
description: "Show others what time it is for you.",
|
||||
aliases: ["tz"],
|
||||
async run({send, author}) {
|
||||
send(getTimeEmbed(author));
|
||||
send({embeds: [getTimeEmbed(author)]});
|
||||
},
|
||||
subcommands: {
|
||||
// Welcome to callback hell. We hope you enjoy your stay here!
|
||||
|
@ -298,10 +296,11 @@ export default new NamedCommand({
|
|||
|
||||
const finalize = () => {
|
||||
Storage.save();
|
||||
send(
|
||||
"You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.",
|
||||
getTimeEmbed(author)
|
||||
);
|
||||
send({
|
||||
content:
|
||||
"You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.",
|
||||
embeds: [getTimeEmbed(author)]
|
||||
});
|
||||
};
|
||||
|
||||
if (hasDST) {
|
||||
|
@ -358,23 +357,25 @@ export default new NamedCommand({
|
|||
const time = moment().utc();
|
||||
|
||||
send({
|
||||
embed: {
|
||||
color: TIME_EMBED_COLOR,
|
||||
fields: [
|
||||
{
|
||||
name: "Local Date",
|
||||
value: time.format(DATE_FORMAT)
|
||||
},
|
||||
{
|
||||
name: "Day of the Week",
|
||||
value: time.format(DOW_FORMAT)
|
||||
},
|
||||
{
|
||||
name: "Local Time",
|
||||
value: time.format(TIME_FORMAT)
|
||||
}
|
||||
]
|
||||
}
|
||||
embeds: [
|
||||
{
|
||||
color: TIME_EMBED_COLOR,
|
||||
fields: [
|
||||
{
|
||||
name: "Local Date",
|
||||
value: time.format(DATE_FORMAT)
|
||||
},
|
||||
{
|
||||
name: "Day of the Week",
|
||||
value: time.format(DOW_FORMAT)
|
||||
},
|
||||
{
|
||||
name: "Local Time",
|
||||
value: time.format(TIME_FORMAT)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
@ -387,14 +388,14 @@ export default new NamedCommand({
|
|||
user: new Command({
|
||||
description: "See what time it is for someone else.",
|
||||
async run({send, args}) {
|
||||
send(getTimeEmbed(args[0]));
|
||||
send({embeds: [getTimeEmbed(args[0])]});
|
||||
}
|
||||
}),
|
||||
any: new RestCommand({
|
||||
description: "See what time it is for someone else (by their username).",
|
||||
async run({send, guild, combined}) {
|
||||
const user = await getUserByNickname(combined, guild);
|
||||
if (typeof user !== "string") send(getTimeEmbed(user));
|
||||
if (typeof user !== "string") send({embeds: [getTimeEmbed(user)]});
|
||||
else send(user);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@ export default new NamedCommand({
|
|||
);
|
||||
}
|
||||
|
||||
send(embed);
|
||||
send({embeds: [embed]});
|
||||
},
|
||||
subcommands: {
|
||||
add: new NamedCommand({
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import {Command, NamedCommand, RestCommand} from "onion-lasers";
|
||||
import translate from "translate-google";
|
||||
|
||||
export default new NamedCommand({
|
||||
description: "Translates your input.",
|
||||
usage: "<lang ID> <input>",
|
||||
run: "You need to specify a language to translate to.",
|
||||
any: new Command({
|
||||
run: "You need to enter some text to translate.",
|
||||
any: new RestCommand({
|
||||
async run({send, args}) {
|
||||
const lang = args[0];
|
||||
const input = args.slice(1).join(" ");
|
||||
translate(input, {
|
||||
to: lang
|
||||
})
|
||||
.then((res) => {
|
||||
send({
|
||||
embed: {
|
||||
title: "Translation",
|
||||
fields: [
|
||||
{
|
||||
name: "Input",
|
||||
value: `\`\`\`${input}\`\`\``
|
||||
},
|
||||
{
|
||||
name: "Output",
|
||||
value: `\`\`\`${res}\`\`\``
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
send(
|
||||
`${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes`
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
declare module "pet-pet-gif" {
|
||||
function petPetGif(image: string): Promise<Buffer>;
|
||||
export = petPetGif;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
interface TranslateOptions {
|
||||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
declare module "translate-google" {
|
||||
function translate(input: string, options: TranslateOptions): Promise<string>;
|
||||
export = translate;
|
||||
}
|
35
src/index.ts
35
src/index.ts
|
@ -1,10 +1,21 @@
|
|||
import "./modules/globals";
|
||||
import {Client, Permissions} from "discord.js";
|
||||
import {Client, Permissions, Intents} from "discord.js";
|
||||
import path from "path";
|
||||
|
||||
// This is here in order to make it much less of a headache to access the client from other files.
|
||||
// This of course won't actually do anything until the setup process is complete and it logs in.
|
||||
export const client = new Client();
|
||||
export const client = new Client({
|
||||
intents: [
|
||||
Intents.FLAGS.GUILDS,
|
||||
Intents.FLAGS.GUILD_MEMBERS,
|
||||
Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
|
||||
Intents.FLAGS.GUILD_VOICE_STATES,
|
||||
Intents.FLAGS.GUILD_PRESENCES,
|
||||
Intents.FLAGS.GUILD_MESSAGES,
|
||||
Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
|
||||
Intents.FLAGS.DIRECT_MESSAGES
|
||||
]
|
||||
});
|
||||
|
||||
import {launch} from "onion-lasers";
|
||||
import setup from "./modules/setup";
|
||||
|
@ -31,20 +42,20 @@ launch(client, path.join(__dirname, "commands"), {
|
|||
name: "Moderator",
|
||||
check: (_user, member) =>
|
||||
!!member &&
|
||||
(member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
|
||||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
|
||||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
|
||||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS))
|
||||
(member.permissions.has(Permissions.FLAGS.MANAGE_ROLES) ||
|
||||
member.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES) ||
|
||||
member.permissions.has(Permissions.FLAGS.KICK_MEMBERS) ||
|
||||
member.permissions.has(Permissions.FLAGS.BAN_MEMBERS))
|
||||
},
|
||||
{
|
||||
// ADMIN //
|
||||
name: "Administrator",
|
||||
check: (_user, member) => !!member && member.hasPermission(Permissions.FLAGS.ADMINISTRATOR)
|
||||
check: (_user, member) => !!member && member.permissions.has(Permissions.FLAGS.ADMINISTRATOR)
|
||||
},
|
||||
{
|
||||
// OWNER //
|
||||
name: "Server Owner",
|
||||
check: (_user, member) => !!member && member.guild.ownerID === member.id
|
||||
check: (_user, member) => !!member && member.guild.ownerId === member.id
|
||||
},
|
||||
{
|
||||
// BOT_SUPPORT //
|
||||
|
@ -67,10 +78,14 @@ launch(client, path.join(__dirname, "commands"), {
|
|||
// Initialize Modules //
|
||||
import "./modules/ready";
|
||||
import "./modules/presence";
|
||||
import "./modules/lavalink";
|
||||
// TODO: Reimplement entire music system, contact Sink
|
||||
// import "./modules/lavalink";
|
||||
import "./modules/emoteRegistry";
|
||||
import "./modules/systemInfo";
|
||||
import "./modules/intercept";
|
||||
import "./modules/messageEmbed";
|
||||
// import "./modules/messageEmbed";
|
||||
import "./modules/guildMemberAdd";
|
||||
import "./modules/streamNotifications";
|
||||
import "./modules/channelDefaults";
|
||||
// This module must be loaded last for the dynamic event reading to work properly.
|
||||
import "./modules/eventLogging";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {strict as assert} from "assert";
|
||||
import {pluralise, pluraliseSigned, replaceAll, toTitleCase, split, parseVars} from "./lib";
|
||||
import {pluralise, pluraliseSigned, replaceAll, toTitleCase, split, parseVars, parseVarsCallback} from "./lib";
|
||||
|
||||
// I can't figure out a way to run the test suite while running the bot.
|
||||
describe("Wrappers", () => {
|
||||
|
@ -58,6 +58,15 @@ describe("Wrappers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#parseVarsCallback()", () => {
|
||||
it('should replace %test% with "yeet"', () => {
|
||||
assert.strictEqual(
|
||||
parseVarsCallback("ya %test% the %pear%", (variable) => (variable === "test" ? "yeet" : "null")),
|
||||
"ya yeet the null"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#toTitleCase()", () => {
|
||||
it("should capitalize the first letter of each word", () => {
|
||||
assert.strictEqual(
|
||||
|
|
42
src/lib.ts
42
src/lib.ts
|
@ -38,15 +38,20 @@ export function parseArgs(line: string): string[] {
|
|||
* - `%%` = `%`
|
||||
* - If the invalid token is null/undefined, nothing is changed.
|
||||
*/
|
||||
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string | null = ""): string {
|
||||
export function parseVars(
|
||||
line: string,
|
||||
definitions: {[key: string]: string},
|
||||
delimiter = "%",
|
||||
invalid: string | null = ""
|
||||
): string {
|
||||
let result = "";
|
||||
let inVariable = false;
|
||||
let token = "";
|
||||
|
||||
for (const c of line) {
|
||||
if (c === "%") {
|
||||
if (c === delimiter) {
|
||||
if (inVariable) {
|
||||
if (token === "") result += "%";
|
||||
if (token === "") result += delimiter;
|
||||
else {
|
||||
if (token in definitions) result += definitions[token];
|
||||
else if (invalid === null) result += `%${token}%`;
|
||||
|
@ -64,6 +69,29 @@ export function parseVars(line: string, definitions: {[key: string]: string}, in
|
|||
return result;
|
||||
}
|
||||
|
||||
export function parseVarsCallback(line: string, callback: (variable: string) => string, delimiter = "%"): string {
|
||||
let result = "";
|
||||
let inVariable = false;
|
||||
let token = "";
|
||||
|
||||
for (const c of line) {
|
||||
if (c === delimiter) {
|
||||
if (inVariable) {
|
||||
if (token === "") result += delimiter;
|
||||
else {
|
||||
result += callback(token);
|
||||
token = "";
|
||||
}
|
||||
}
|
||||
|
||||
inVariable = !inVariable;
|
||||
} else if (inVariable) token += c;
|
||||
else result += c;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isType(value: any, type: any): boolean {
|
||||
if (value === undefined && type === undefined) return true;
|
||||
else if (value === null && type === null) return true;
|
||||
|
@ -86,7 +114,7 @@ export function select<T>(value: any, fallback: T, type: Function, isArray = fal
|
|||
}
|
||||
}
|
||||
|
||||
export function clean(text: any) {
|
||||
export function clean(text: unknown) {
|
||||
if (typeof text === "string")
|
||||
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
|
||||
else return text;
|
||||
|
@ -126,7 +154,11 @@ export function getContent(url: string): Promise<{url: string}> {
|
|||
const parsedData = JSON.parse(rawData);
|
||||
resolve(parsedData);
|
||||
} catch (e) {
|
||||
reject(`Error: ${e.message}`);
|
||||
let errorMessage = "Something went wrong! We don't know what, though...";
|
||||
if (e instanceof Error) {
|
||||
errorMessage = e.message;
|
||||
}
|
||||
reject(`Error: ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
}).on("error", (err: {message: any}) => {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import {client} from "../index";
|
||||
import {Storage} from "../structures";
|
||||
import {Permissions} from "discord.js";
|
||||
|
||||
client.on("voiceStateUpdate", async (before, after) => {
|
||||
const channel = before.channel;
|
||||
const {channelNames} = Storage.getGuild(after.guild.id);
|
||||
|
||||
if (
|
||||
channel &&
|
||||
channel.members.size === 0 &&
|
||||
channel.id in channelNames &&
|
||||
before.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS)
|
||||
) {
|
||||
channel.setName(channelNames[channel.id]);
|
||||
}
|
||||
});
|
|
@ -2,17 +2,18 @@ import {client} from "../index";
|
|||
import FileManager from "./storage";
|
||||
import {EmoteRegistryDump} from "../structures";
|
||||
|
||||
function updateGlobalEmoteRegistry(): void {
|
||||
async function updateGlobalEmoteRegistry(): Promise<void> {
|
||||
const data: EmoteRegistryDump = {version: 1, list: []};
|
||||
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
for (const emote of guild.emojis.cache.values()) {
|
||||
let g = await guild.fetch();
|
||||
for (const emote of g.emojis.cache.values()) {
|
||||
data.list.push({
|
||||
ref: emote.name,
|
||||
id: emote.id,
|
||||
name: emote.name,
|
||||
requires_colons: emote.requiresColons || false,
|
||||
animated: emote.animated,
|
||||
requires_colons: emote.requiresColons ?? false,
|
||||
animated: emote.animated ?? false,
|
||||
url: emote.url,
|
||||
guild_id: emote.guild.name,
|
||||
guild_name: emote.guild.name
|
||||
|
@ -20,34 +21,13 @@ function updateGlobalEmoteRegistry(): void {
|
|||
}
|
||||
}
|
||||
|
||||
FileManager.write("emote-registry", data, true);
|
||||
FileManager.open("data/public"); // generate folder if it doesn't exist
|
||||
FileManager.write("public/emote-registry", data, false);
|
||||
}
|
||||
|
||||
client.on("emojiCreate", (emote) => {
|
||||
console.log(`Updated emote registry. ${emote.name}`);
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
|
||||
client.on("emojiDelete", () => {
|
||||
console.log("Updated emote registry.");
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
|
||||
client.on("emojiUpdate", () => {
|
||||
console.log("Updated emote registry.");
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
|
||||
client.on("guildCreate", () => {
|
||||
console.log("Updated emote registry.");
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
|
||||
client.on("guildDelete", () => {
|
||||
console.log("Updated emote registry.");
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
|
||||
client.on("ready", () => {
|
||||
updateGlobalEmoteRegistry();
|
||||
});
|
||||
client.on("emojiCreate", updateGlobalEmoteRegistry);
|
||||
client.on("emojiDelete", updateGlobalEmoteRegistry);
|
||||
client.on("emojiUpdate", updateGlobalEmoteRegistry);
|
||||
client.on("guildCreate", updateGlobalEmoteRegistry);
|
||||
client.on("guildDelete", updateGlobalEmoteRegistry);
|
||||
client.on("ready", updateGlobalEmoteRegistry);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// This will keep track of the last event that occurred to provide context to error messages.
|
||||
// Like with logging each command invocation, it's not a good idea to pollute the logs with this kind of stuff when it works most of the time.
|
||||
// However, it's also a pain to debug when no context is provided for an error message.
|
||||
import {client} from "..";
|
||||
import {setExecuteCommandListener} from "onion-lasers";
|
||||
import {TextChannel, DMChannel, NewsChannel} from "discord.js";
|
||||
|
||||
let lastEvent = "N/A";
|
||||
let lastCommandInfo: {
|
||||
header: string;
|
||||
args: string[];
|
||||
channel: TextChannel | DMChannel | NewsChannel | null;
|
||||
} = {
|
||||
header: "N/A",
|
||||
args: [],
|
||||
channel: null
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason: any) => {
|
||||
const isLavalinkError = reason?.code === "ECONNREFUSED";
|
||||
const isDiscordError = reason?.name === "DiscordAPIError";
|
||||
|
||||
if (!isLavalinkError) {
|
||||
// If it's a DiscordAPIError on a message event, I'll make the assumption that it comes from the command handler.
|
||||
// That's not always the case though, especially if you add your own message events. Just be wary of that.
|
||||
if (isDiscordError && lastEvent === "message") {
|
||||
console.error(
|
||||
`Command Error: ${lastCommandInfo.header} (${lastCommandInfo.args.join(", ")})\n${reason.stack}`
|
||||
);
|
||||
lastCommandInfo.channel?.send(
|
||||
`There was an error while trying to execute that command!\`\`\`${reason.stack}\`\`\``
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`@${lastEvent} : /${lastCommandInfo.header} (${lastCommandInfo.args.join(", ")})\n${reason.stack}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store info on which command was executed last.
|
||||
setExecuteCommandListener(({header, args, channel}) => {
|
||||
lastCommandInfo = {
|
||||
header,
|
||||
args,
|
||||
channel
|
||||
};
|
||||
});
|
||||
|
||||
// This will dynamically attach all known events instead of doing it manually.
|
||||
// As such, it needs to be placed after all other events are attached or the tracking won't be done properly.
|
||||
for (const event of client.eventNames()) {
|
||||
client.on(event, () => {
|
||||
lastEvent = event.toString();
|
||||
});
|
||||
}
|
|
@ -3,6 +3,7 @@ import chalk from "chalk";
|
|||
declare global {
|
||||
var IS_DEV_MODE: boolean;
|
||||
var PERMISSIONS: typeof PermissionsEnum;
|
||||
var BOT_VERSION: string;
|
||||
|
||||
interface Console {
|
||||
ready: (...data: any[]) => void;
|
||||
|
@ -21,6 +22,7 @@ enum PermissionsEnum {
|
|||
|
||||
global.IS_DEV_MODE = process.argv[2] === "dev";
|
||||
global.PERMISSIONS = PermissionsEnum;
|
||||
global.BOT_VERSION = "3.2.3";
|
||||
|
||||
const oldConsole = console;
|
||||
|
||||
|
|
|
@ -16,12 +16,16 @@ function applyText(canvas: Canvas, text: string) {
|
|||
}
|
||||
|
||||
client.on("guildMemberAdd", async (member) => {
|
||||
const {welcomeType, welcomeChannel, welcomeMessage} = Storage.getGuild(member.guild.id);
|
||||
const {welcomeType, welcomeChannel, welcomeMessage, autoRoles} = Storage.getGuild(member.guild.id);
|
||||
|
||||
if (autoRoles) {
|
||||
member.roles.add(autoRoles);
|
||||
}
|
||||
|
||||
if (welcomeChannel) {
|
||||
const channel = member.guild.channels.cache.get(welcomeChannel);
|
||||
|
||||
if (channel && channel.type === "text") {
|
||||
if (channel && channel instanceof TextChannel) {
|
||||
if (welcomeType === "graphical") {
|
||||
const canvas = createCanvas(700, 250);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
@ -56,9 +60,9 @@ client.on("guildMemberAdd", async (member) => {
|
|||
ctx.drawImage(avatar, 25, 25, 200, 200);
|
||||
|
||||
const attachment = new MessageAttachment(canvas.toBuffer("image/png"), "welcome-image.png");
|
||||
(channel as TextChannel).send(`Welcome \`${member.user.tag}\`!`, attachment);
|
||||
channel.send({content: `Welcome \`${member.user.tag}\`!`, attachments: [attachment]});
|
||||
} else if (welcomeType === "text") {
|
||||
(channel as TextChannel).send(
|
||||
channel.send(
|
||||
parseVars(
|
||||
welcomeMessage || "Say hello to `%user%`, everyone! We all need a warm welcome sometimes :D",
|
||||
{
|
||||
|
@ -68,7 +72,7 @@ client.on("guildMemberAdd", async (member) => {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
console.error(`"${welcomeChannel}" is not a valid text channel ID!`);
|
||||
console.error("[modules/guildMemberAdd]", `"${welcomeChannel}" is not a valid text channel ID!`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import {client} from "../index";
|
||||
|
||||
// Potentially port CE's intercept module to here?
|
||||
// - ` ${text} `.test(/[ \.,\?!]hon[ \.,\?!]/)
|
||||
// - "oil" will remain the same though, it's better that way (anything even remotely "oil"-related calls the image)
|
||||
// - Also uwu and owo penalties
|
||||
|
||||
client.on("message", (message) => {
|
||||
if (message.content.toLowerCase().includes("remember to drink water")) {
|
||||
message.react("🚱");
|
||||
|
|
|
@ -1,48 +1,49 @@
|
|||
import attachClientToLavalink from "discord.js-lavalink-lib";
|
||||
import {Config} from "../structures";
|
||||
import {client} from "../index";
|
||||
|
||||
// Although the example showed to do "client.music = LavaLink(...)" and "(client as any).music = Lavalink(...)" was done to match that, nowhere in the library is client.music ever actually used nor does the function return anything. In other words, client.music is undefined and is never used.
|
||||
attachClientToLavalink(client, {
|
||||
lavalink: {
|
||||
restnode: {
|
||||
host: "localhost",
|
||||
port: 2333,
|
||||
password: "youshallnotpass"
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
host: "localhost",
|
||||
port: 2333,
|
||||
password: "youshallnotpass"
|
||||
}
|
||||
]
|
||||
},
|
||||
prefix: Config.prefix,
|
||||
helpCmd: "mhelp",
|
||||
admins: ["717352467280691331"]
|
||||
});
|
||||
|
||||
// Disable the unhandledRejection listener by Lavalink because it captures every single unhandled
|
||||
// rejection and adds its message with it. Then replace it with a better, more selective error handler.
|
||||
for (const listener of process.listeners("unhandledRejection")) {
|
||||
if (listener.toString().includes("discord.js-lavalink-musicbot")) {
|
||||
process.off("unhandledRejection", listener);
|
||||
}
|
||||
}
|
||||
|
||||
process.on("unhandledRejection", (reason: any) => {
|
||||
if (reason?.code === "ECONNREFUSED") {
|
||||
console.error(
|
||||
`[discord.js-lavalink-musicbot] Caught unhandled rejection: ${reason.stack}\nIf this is causing issues, head to the support server at https://discord.gg/dNN4azK`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// It's unsafe to process uncaughtException because after an uncaught exception, the system
|
||||
// becomes corrupted. So disable Lavalink from adding a hook to it.
|
||||
for (const listener of process.listeners("uncaughtException")) {
|
||||
if (listener.toString().includes("discord.js-lavalink-musicbot")) {
|
||||
process.off("uncaughtException", listener);
|
||||
}
|
||||
}
|
||||
// import attachClientToLavalink from "discord.js-lavalink-lib";
|
||||
// import {Config} from "../structures";
|
||||
// import {client} from "../index";
|
||||
//
|
||||
// // Although the example showed to do "client.music = LavaLink(...)" and "(client as any).music = Lavalink(...)" was done to match that, nowhere in the library is client.music ever actually used nor does the function return anything. In other words, client.music is undefined and is never used.
|
||||
// attachClientToLavalink(client, {
|
||||
// lavalink: {
|
||||
// restnode: {
|
||||
// host: "localhost",
|
||||
// port: 2333,
|
||||
// password: "youshallnotpass"
|
||||
// },
|
||||
// nodes: [
|
||||
// {
|
||||
// host: "localhost",
|
||||
// port: 2333,
|
||||
// password: "youshallnotpass"
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// prefix: Config.prefix,
|
||||
// helpCmd: "mhelp",
|
||||
// admins: ["717352467280691331"]
|
||||
// });
|
||||
//
|
||||
// // Disable the unhandledRejection listener by Lavalink because it captures every single unhandled
|
||||
// // rejection and adds its message with it. Then replace it with a better, more selective error handler.
|
||||
// for (const listener of process.listeners("unhandledRejection")) {
|
||||
// if (listener.toString().includes("discord.js-lavalink-musicbot")) {
|
||||
// process.off("unhandledRejection", listener);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// process.on("unhandledRejection", (reason: any) => {
|
||||
// if (reason?.code === "ECONNREFUSED") {
|
||||
// // This is console.warn instead of console.error because on development environments, unless Lavalink is being tested, it won't interfere with the bot's functionality.
|
||||
// console.warn(
|
||||
// `[discord.js-lavalink-musicbot] Caught unhandled rejection: ${reason.stack}\nIf this is causing issues, head to the support server at https://discord.gg/dNN4azK`
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// // It's unsafe to process uncaughtException because after an uncaught exception, the system
|
||||
// // becomes corrupted. So disable Lavalink from adding a hook to it.
|
||||
// for (const listener of process.listeners("uncaughtException")) {
|
||||
// if (listener.toString().includes("discord.js-lavalink-musicbot")) {
|
||||
// process.off("uncaughtException", listener);
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -3,45 +3,47 @@ import {MessageEmbed} from "discord.js";
|
|||
import {getPrefix} from "../structures";
|
||||
import {getMessageByID} from "onion-lasers";
|
||||
|
||||
client.on("message", async (message) => {
|
||||
// Only execute if the message is from a user and isn't a command.
|
||||
if (message.content.startsWith(getPrefix(message.guild)) || message.author.bot) return;
|
||||
const messageLink = extractFirstMessageLink(message.content);
|
||||
if (!messageLink) return;
|
||||
const [guildID, channelID, messageID] = messageLink;
|
||||
client.on("message", (message) => {
|
||||
(async () => {
|
||||
// Only execute if the message is from a user and isn't a command.
|
||||
if (message.content.startsWith(getPrefix(message.guild)) || message.author.bot) return;
|
||||
const messageLink = extractFirstMessageLink(message.content);
|
||||
if (!messageLink) return;
|
||||
const [guildID, channelID, messageID] = messageLink;
|
||||
|
||||
const linkMessage = await getMessageByID(channelID, messageID);
|
||||
const linkMessage = await getMessageByID(channelID, messageID);
|
||||
|
||||
// If it's an invalid link (or the bot doesn't have access to it).
|
||||
if (typeof linkMessage === "string") {
|
||||
return message.channel.send("I don't have access to that channel!");
|
||||
}
|
||||
// If it's an invalid link (or the bot doesn't have access to it).
|
||||
if (typeof linkMessage === "string") {
|
||||
return message.channel.send("I don't have access to that channel!");
|
||||
}
|
||||
|
||||
const embeds = [
|
||||
...linkMessage.embeds.filter((embed) => embed.type === "rich"),
|
||||
...linkMessage.attachments.values()
|
||||
];
|
||||
const embeds = [
|
||||
...linkMessage.embeds.filter((embed) => embed.type === "rich"),
|
||||
...linkMessage.attachments.values()
|
||||
];
|
||||
|
||||
if (!linkMessage.cleanContent && embeds.length === 0) {
|
||||
return message.channel.send(new MessageEmbed().setDescription("🚫 The message is empty."));
|
||||
}
|
||||
if (!linkMessage.cleanContent && embeds.length === 0) {
|
||||
return message.channel.send({embeds: [new MessageEmbed().setDescription("🚫 The message is empty.")]});
|
||||
}
|
||||
|
||||
const infoEmbed = new MessageEmbed()
|
||||
.setAuthor(
|
||||
linkMessage.author.username,
|
||||
linkMessage.author.displayAvatarURL({format: "png", dynamic: true, size: 4096})
|
||||
)
|
||||
.setTimestamp(linkMessage.createdTimestamp)
|
||||
.setDescription(
|
||||
`${linkMessage.cleanContent}\n\nSent in **${linkMessage.guild?.name}** | <#${linkMessage.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))`
|
||||
);
|
||||
const infoEmbed = new MessageEmbed()
|
||||
.setAuthor(
|
||||
linkMessage.author.username,
|
||||
linkMessage.author.displayAvatarURL({format: "png", dynamic: true, size: 4096})
|
||||
)
|
||||
.setTimestamp(linkMessage.createdTimestamp)
|
||||
.setDescription(
|
||||
`${linkMessage.cleanContent}\n\nSent in **${linkMessage.guild?.name}** | <#${linkMessage.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))`
|
||||
);
|
||||
|
||||
if (linkMessage.attachments.size !== 0) {
|
||||
const image = linkMessage.attachments.first();
|
||||
infoEmbed.setImage(image!.url);
|
||||
}
|
||||
if (linkMessage.attachments.size !== 0) {
|
||||
const image = linkMessage.attachments.first();
|
||||
infoEmbed.setImage(image!.url);
|
||||
}
|
||||
|
||||
return await message.channel.send(infoEmbed);
|
||||
return await message.channel.send({embeds: [infoEmbed]});
|
||||
})();
|
||||
});
|
||||
|
||||
export function extractFirstMessageLink(message: string): [string, string, string] | null {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {Config, Storage} from "../structures";
|
|||
client.once("ready", () => {
|
||||
if (client.user) {
|
||||
console.ready(
|
||||
`Logged in as ${client.user.tag}, ready to serve ${client.users.cache.size} users in ${client.guilds.cache.size} servers..`
|
||||
`Logged in as ${client.user.tag}, ready to serve ${client.users.cache.size} users in ${client.guilds.cache.size} servers.`
|
||||
);
|
||||
client.user.setActivity({
|
||||
type: "LISTENING",
|
||||
|
|
|
@ -3,16 +3,6 @@ import inquirer from "inquirer";
|
|||
import Storage from "./storage";
|
||||
import {Config} from "../structures";
|
||||
|
||||
// A generic process handler is set to catch unhandled rejections other than the ones from Lavalink and Discord.
|
||||
process.on("unhandledRejection", (reason: any) => {
|
||||
const isLavalinkError = reason?.code === "ECONNREFUSED";
|
||||
const isDiscordError = reason?.name === "DiscordAPIError";
|
||||
|
||||
if (!isLavalinkError && !isDiscordError) {
|
||||
console.error(reason.stack);
|
||||
}
|
||||
});
|
||||
|
||||
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
|
||||
// And that file won't be written until the data is successfully initialized.
|
||||
const prompts = [
|
||||
|
|
|
@ -14,18 +14,18 @@ const Storage = {
|
|||
data = JSON.parse(file);
|
||||
} catch (error) {
|
||||
if (process.argv[2] !== "dev") {
|
||||
console.warn(`Malformed JSON data (header: ${header}), backing it up.`, file);
|
||||
fs.writeFile(
|
||||
`${path}.backup`,
|
||||
file,
|
||||
generateHandler(`Backup file of "${header}" successfully written as ${file}.`)
|
||||
);
|
||||
console.warn("[storage.read]", `Malformed JSON data (header: ${header}), backing it up.`, file);
|
||||
fs.writeFile(`${path}.backup`, file, (error) => {
|
||||
if (error) console.error("[storage.read]", error);
|
||||
console.log("[storage.read]", `Backup file of "${header}" successfully written as ${file}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
// There is no need to log successfully written operations as it pollutes the log with useless info for debugging.
|
||||
write(header: string, data: object, asynchronous = true) {
|
||||
this.open("data");
|
||||
const path = `data/${header}.json`;
|
||||
|
@ -34,12 +34,17 @@ const Storage = {
|
|||
const result = JSON.stringify(data, null, "\t");
|
||||
|
||||
if (asynchronous)
|
||||
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully spaced and written.`));
|
||||
fs.writeFile(path, result, (error) => {
|
||||
if (error) console.error("[storage.write]", error);
|
||||
});
|
||||
else fs.writeFileSync(path, result);
|
||||
} else {
|
||||
const result = JSON.stringify(data);
|
||||
|
||||
if (asynchronous) fs.writeFile(path, result, generateHandler(`"${header}" sucessfully written.`));
|
||||
if (asynchronous)
|
||||
fs.writeFile(path, result, (error) => {
|
||||
if (error) console.error("[storage.write]", error);
|
||||
});
|
||||
else fs.writeFileSync(path, result);
|
||||
}
|
||||
},
|
||||
|
@ -54,15 +59,10 @@ const Storage = {
|
|||
},
|
||||
close(path: string) {
|
||||
if (fs.existsSync(path) && fs.readdirSync(path).length === 0)
|
||||
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
|
||||
fs.rmdir(path, (error) => {
|
||||
if (error) console.error("[storage.close]", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function generateHandler(message: string) {
|
||||
return (error: Error | null) => {
|
||||
if (error) console.error(error);
|
||||
else console.debug(message);
|
||||
};
|
||||
}
|
||||
|
||||
export default Storage;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Message, Collection} from "discord.js";
|
||||
import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Message, Collection, StageChannel} from "discord.js";
|
||||
import {client} from "../index";
|
||||
import {Storage} from "../structures";
|
||||
|
||||
type Stream = {
|
||||
streamer: GuildMember;
|
||||
channel: VoiceChannel;
|
||||
channel: VoiceChannel | StageChannel;
|
||||
category: string;
|
||||
description?: string;
|
||||
thumbnail?: string;
|
||||
|
@ -19,7 +19,7 @@ export const streamList = new Collection<string, Stream>();
|
|||
// Probably find a better, DRY way of doing this.
|
||||
function getStreamEmbed(
|
||||
streamer: GuildMember,
|
||||
channel: VoiceChannel,
|
||||
channel: VoiceChannel | StageChannel,
|
||||
streamStart: number,
|
||||
category: string,
|
||||
description?: string,
|
||||
|
@ -38,7 +38,7 @@ function getStreamEmbed(
|
|||
// I decided to not include certain fields:
|
||||
// .addField("Activity", "CrossCode", true) - Probably too much presence data involved, increasing memory usage.
|
||||
// .addField("Viewers", 5, true) - There doesn't seem to currently be a way to track how many viewers there are. Presence data for "WATCHING" doesn't seem to affect it, and listening to raw client events doesn't seem to make it appear either.
|
||||
.addField("Voice Channel", channel, true)
|
||||
.addField("Voice Channel", channel.toString(), true)
|
||||
.addField("Category", category, true)
|
||||
.setColor(streamer.displayColor)
|
||||
.setFooter(
|
||||
|
@ -90,21 +90,23 @@ client.on("voiceStateUpdate", async (before, after) => {
|
|||
streamer: member,
|
||||
channel: voiceChannel,
|
||||
category,
|
||||
message: await textChannel.send(
|
||||
streamNotificationPing,
|
||||
getStreamEmbed(member, voiceChannel, streamStart, category)
|
||||
),
|
||||
message: await textChannel.send({
|
||||
content: streamNotificationPing,
|
||||
embeds: [getStreamEmbed(member, voiceChannel, streamStart, category)]
|
||||
}),
|
||||
update(this: Stream) {
|
||||
this.message.edit(
|
||||
getStreamEmbed(
|
||||
this.streamer,
|
||||
this.channel,
|
||||
streamStart,
|
||||
this.category,
|
||||
this.description,
|
||||
this.thumbnail
|
||||
)
|
||||
);
|
||||
this.message.edit({
|
||||
embeds: [
|
||||
getStreamEmbed(
|
||||
this.streamer,
|
||||
this.channel,
|
||||
streamStart,
|
||||
this.category,
|
||||
this.description,
|
||||
this.thumbnail
|
||||
)
|
||||
]
|
||||
});
|
||||
},
|
||||
streamStart
|
||||
});
|
||||
|
@ -125,7 +127,7 @@ client.on("voiceStateUpdate", async (before, after) => {
|
|||
});
|
||||
|
||||
client.on("channelUpdate", (before, after) => {
|
||||
if (before.type === "voice" && after.type === "voice") {
|
||||
if (before instanceof VoiceChannel && after instanceof VoiceChannel) {
|
||||
for (const stream of streamList.values()) {
|
||||
if (after.id === stream.channel.id) {
|
||||
stream.update();
|
||||
|
|
|
@ -1,40 +1,21 @@
|
|||
import {client} from "../index";
|
||||
import {GuildChannel, TextChannel} from "discord.js";
|
||||
import {TextChannel} from "discord.js";
|
||||
import {Config} from "../structures";
|
||||
|
||||
client.on("channelCreate", async (channel) => {
|
||||
const botGuilds = client.guilds;
|
||||
// Logging which guilds the bot is added to and removed from makes sense.
|
||||
// However, logging the specific channels that are added/removed is a tad bit privacy-invading.
|
||||
|
||||
if (channel instanceof GuildChannel) {
|
||||
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
||||
console.log(`Channel created in '${createdGuild.name}' called '#${channel.name}'`);
|
||||
}
|
||||
});
|
||||
client.on("guildCreate", async (guild) => {
|
||||
const owner = await guild.fetchOwner();
|
||||
|
||||
client.on("channelDelete", async (channel) => {
|
||||
const botGuilds = client.guilds;
|
||||
|
||||
if (channel instanceof GuildChannel) {
|
||||
const createdGuild = await botGuilds.fetch(channel.guild.id);
|
||||
console.log(`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("guildCreate", (guild) => {
|
||||
console.log(
|
||||
`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot. Owner: ${guild.owner!.user.tag} (${
|
||||
guild.owner!.user.id
|
||||
}).`
|
||||
);
|
||||
console.log(`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot. Owner: ${owner.user.tag} (${owner.user.id}).`);
|
||||
|
||||
if (Config.systemLogsChannel) {
|
||||
const channel = client.channels.cache.get(Config.systemLogsChannel);
|
||||
|
||||
if (channel && channel.type === "text") {
|
||||
(channel as TextChannel).send(
|
||||
`TravBot joined: \`${guild.name}\`. The owner of this guild is: \`${guild.owner!.user.tag}\` (\`${
|
||||
guild.owner!.user.id
|
||||
}\`)`
|
||||
if (channel instanceof TextChannel) {
|
||||
channel.send(
|
||||
`TravBot joined: \`${guild.name}\`. The owner of this guild is: \`${owner.user.tag}\` (\`${owner.user.id}\`)`
|
||||
);
|
||||
} else {
|
||||
console.warn(`${Config.systemLogsChannel} is not a valid text channel for system logs!`);
|
||||
|
@ -48,10 +29,14 @@ client.on("guildDelete", (guild) => {
|
|||
if (Config.systemLogsChannel) {
|
||||
const channel = client.channels.cache.get(Config.systemLogsChannel);
|
||||
|
||||
if (channel && channel.type === "text") {
|
||||
(channel as TextChannel).send(`\`${guild.name}\` (\`${guild.id}\`) removed the bot.`);
|
||||
if (channel instanceof TextChannel) {
|
||||
channel.send(`\`${guild.name}\` (\`${guild.id}\`) removed the bot.`);
|
||||
} else {
|
||||
console.warn(`${Config.systemLogsChannel} is not a valid text channel for system logs!`);
|
||||
console.warn(
|
||||
`${Config.systemLogsChannel} is not a valid text channel for system logs! Removing it from storage.`
|
||||
);
|
||||
Config.systemLogsChannel = null;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import {Webhook, TextChannel, NewsChannel, Permissions, Collection} from "discord.js";
|
||||
import {client} from "..";
|
||||
import {Config} from "../structures";
|
||||
|
||||
export const webhookStorage = new Collection<string, Webhook>(); // Channel ID: Webhook
|
||||
const WEBHOOK_PATTERN = /https:\/\/discord\.com\/api\/webhooks\/(\d{17,})\/(.+)/;
|
||||
const ID_PATTERN = /(\d{17,})/;
|
||||
|
||||
// Resolve any available webhooks available for a selected channel.
|
||||
export async function resolveWebhook(channel: TextChannel | NewsChannel): Promise<Webhook | null> {
|
||||
if (channel.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_WEBHOOKS)) {
|
||||
const webhooksInChannel = await channel.fetchWebhooks();
|
||||
|
||||
if (webhooksInChannel.size > 0) return webhooksInChannel.first()!;
|
||||
else return null;
|
||||
}
|
||||
|
||||
for (const [channelID, webhook] of webhookStorage.entries()) if (channel.id === channelID) return webhook;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function registerWebhook(url: string): boolean {
|
||||
if (WEBHOOK_PATTERN.test(url)) {
|
||||
const [_, id, token] = WEBHOOK_PATTERN.exec(url)!;
|
||||
Config.webhooks[id] = token;
|
||||
Config.save();
|
||||
refreshWebhookCache();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteWebhook(urlOrID: string): boolean {
|
||||
let id: string | null = null;
|
||||
|
||||
if (WEBHOOK_PATTERN.test(urlOrID)) id = WEBHOOK_PATTERN.exec(urlOrID)![1];
|
||||
else if (ID_PATTERN.test(urlOrID)) id = ID_PATTERN.exec(urlOrID)![1];
|
||||
|
||||
if (id) {
|
||||
delete Config.webhooks[id];
|
||||
Config.save();
|
||||
refreshWebhookCache();
|
||||
}
|
||||
|
||||
return !!id;
|
||||
}
|
||||
|
||||
// This will return the target channel of a webhook create/edit/delete event.
|
||||
// No permission is needed to receive this event, but since you only get the target channel, all stored webhooks must be fetched again.
|
||||
// You can't rely on guilds giving the bot the manage webhooks permission.
|
||||
client.on("webhookUpdate", refreshWebhookCache);
|
||||
client.on("ready", refreshWebhookCache);
|
||||
|
||||
// Reload webhook objects from the storage.
|
||||
export async function refreshWebhookCache(): Promise<void> {
|
||||
webhookStorage.clear();
|
||||
|
||||
for (const [id, token] of Object.entries(Config.webhooks)) {
|
||||
// If there are stored webhook IDs/tokens that don't work, delete those webhooks from storage.
|
||||
try {
|
||||
const webhook = await client.fetchWebhook(id, token);
|
||||
webhookStorage.set(webhook.channelId, webhook);
|
||||
} catch {
|
||||
delete Config.webhooks[id];
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,10 @@ class ConfigStructure extends GenericStructure {
|
|||
public owner: string;
|
||||
public admins: string[];
|
||||
public support: string[];
|
||||
public lavalink: boolean | null;
|
||||
public wolfram: string | null;
|
||||
public systemLogsChannel: string | null;
|
||||
public webhooks: {[id: string]: string}; // id-token pairs
|
||||
|
||||
constructor(data: GenericJSON) {
|
||||
super("config");
|
||||
|
@ -22,7 +25,18 @@ class ConfigStructure extends GenericStructure {
|
|||
this.owner = select(data.owner, "", String);
|
||||
this.admins = select(data.admins, [], String, true);
|
||||
this.support = select(data.support, [], String, true);
|
||||
this.lavalink = select(data.lavalink, null, Boolean);
|
||||
this.wolfram = select(data.wolfram, null, String);
|
||||
this.systemLogsChannel = select(data.systemLogsChannel, null, String);
|
||||
this.webhooks = {};
|
||||
|
||||
for (const id in data.webhooks) {
|
||||
const token = data.webhooks[id];
|
||||
|
||||
if (/\d{17,}/g.test(id) && typeof token === "string") {
|
||||
this.webhooks[id] = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,19 +81,25 @@ class Member {
|
|||
|
||||
class Guild {
|
||||
public prefix: string | null;
|
||||
public messageEmbeds: boolean | null;
|
||||
public welcomeType: "none" | "text" | "graphical";
|
||||
public welcomeChannel: string | null;
|
||||
public welcomeMessage: string | null;
|
||||
public autoRoles: string[] | null; // StringArray of role IDs
|
||||
public streamingChannel: string | null;
|
||||
public streamingRoles: {[role: string]: string}; // Role ID: Category Name
|
||||
public channelNames: {[channel: string]: string};
|
||||
public members: {[id: string]: Member};
|
||||
|
||||
constructor(data?: GenericJSON) {
|
||||
this.prefix = select(data?.prefix, null, String);
|
||||
this.messageEmbeds = select(data?.messageLinks, true, Boolean);
|
||||
this.welcomeChannel = select(data?.welcomeChannel, null, String);
|
||||
this.welcomeMessage = select(data?.welcomeMessage, null, String);
|
||||
this.autoRoles = select(data?.autoRoles, null, String, true);
|
||||
this.streamingChannel = select(data?.streamingChannel, null, String);
|
||||
this.streamingRoles = {};
|
||||
this.channelNames = {};
|
||||
this.members = {};
|
||||
|
||||
switch (data?.welcomeType) {
|
||||
|
@ -104,6 +124,16 @@ class Guild {
|
|||
}
|
||||
}
|
||||
|
||||
if (data?.channelNames) {
|
||||
for (const id in data.channelNames) {
|
||||
const name = data.channelNames[id];
|
||||
|
||||
if (/\d{17,}/g.test(id) && typeof name === "string") {
|
||||
this.channelNames[id] = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.members) {
|
||||
for (let id in data.members) {
|
||||
if (/\d{17,}/g.test(id)) {
|
||||
|
@ -116,7 +146,10 @@ class Guild {
|
|||
/** Gets a member's profile if they exist and generate one if not. */
|
||||
public getMember(id: string): Member {
|
||||
if (!/\d{17,}/g.test(id))
|
||||
console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`);
|
||||
console.warn(
|
||||
"[structures]",
|
||||
`"${id}" is not a valid user ID! It will be erased when the data loads again.`
|
||||
);
|
||||
|
||||
if (id in this.members) return this.members[id];
|
||||
else {
|
||||
|
@ -142,7 +175,10 @@ class StorageStructure extends GenericStructure {
|
|||
/** Gets a user's profile if they exist and generate one if not. */
|
||||
public getUser(id: string): User {
|
||||
if (!/\d{17,}/g.test(id))
|
||||
console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`);
|
||||
console.warn(
|
||||
"[structures]",
|
||||
`"${id}" is not a valid user ID! It will be erased when the data loads again.`
|
||||
);
|
||||
|
||||
if (id in this.users) return this.users[id];
|
||||
else {
|
||||
|
@ -155,7 +191,10 @@ class StorageStructure extends GenericStructure {
|
|||
/** Gets a guild's settings if they exist and generate one if not. */
|
||||
public getGuild(id: string): Guild {
|
||||
if (!/\d{17,}/g.test(id))
|
||||
console.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`);
|
||||
console.warn(
|
||||
"[structures]",
|
||||
`"${id}" is not a valid guild ID! It will be erased when the data loads again.`
|
||||
);
|
||||
|
||||
if (id in this.guilds) return this.guilds[id];
|
||||
else {
|
||||
|
@ -173,8 +212,7 @@ export let Storage = new StorageStructure(FileManager.read("storage"));
|
|||
// This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache.
|
||||
// However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues.
|
||||
if (IS_DEV_MODE) {
|
||||
watch("data", (event, filename) => {
|
||||
console.debug("File Watcher:", event, filename);
|
||||
watch("data", (_event, filename) => {
|
||||
const header = filename.substring(0, filename.indexOf(".json"));
|
||||
|
||||
switch (header) {
|
||||
|
@ -207,9 +245,9 @@ export function getPrefix(guild: DiscordGuild | null): string {
|
|||
}
|
||||
|
||||
export interface EmoteRegistryDumpEntry {
|
||||
ref: string;
|
||||
ref: string | null;
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
name: string | null;
|
||||
requires_colons: boolean;
|
||||
animated: boolean;
|
||||
url: string;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
// Type Settings //
|
||||
"strict": true, // Enables all strict checks possible.
|
||||
"noImplicitReturns": true, // Makes sure you don't accidentally return something + undefined.
|
||||
"noImplicitReturns": false, // Makes sure you don't accidentally return something + undefined.
|
||||
"noFallthroughCasesInSwitch": true, // Prevents accidentally forgetting to break every switch case. Of course, if you know what you're doing, feel free to add a @ts-ignore, which also signals that it's not a mistake.
|
||||
"forceConsistentCasingInFileNames": true, // Make import paths case-sensitive. "./tEst" is no longer the same as "./test".
|
||||
"esModuleInterop": true, // Enables compatibility with Node.js' module system since the entire export can be whatever you want. allowSyntheticDefaultImports doesn't address runtime issues and is made redundant by this setting.
|
||||
|
|
Loading…
Reference in New Issue