mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Merge branch 'typescript' into HEAD
This commit is contained in:
commit
dd572e637d
9 changed files with 362 additions and 57 deletions
10
.github/workflows/image.yml
vendored
10
.github/workflows/image.yml
vendored
|
@ -3,7 +3,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- typescript
|
||||
- docker
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
@ -16,10 +15,13 @@ jobs:
|
|||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.JS
|
||||
uses: actions/setup-node@v2-beta
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "12"
|
||||
- run: npm ci
|
||||
node-version: "14"
|
||||
# https://github.com/npm/cli/issues/558#issuecomment-580018468
|
||||
# Error: "npm ERR! fsevents not accessible from jest-haste-map"
|
||||
# (supposed to just be a warning b/c optional dependency, but CI environment causes it to fail)
|
||||
- run: npm i
|
||||
|
||||
- name: Build codebase
|
||||
run: npm run build
|
||||
|
|
67
Dockerfile
67
Dockerfile
|
@ -1,4 +1,69 @@
|
|||
FROM node:current-alpine
|
||||
###############
|
||||
# Solution #1 #
|
||||
###############
|
||||
# https://github.com/geekduck/docker-node-canvas
|
||||
# Took 20m 55s
|
||||
|
||||
#FROM node:12
|
||||
#
|
||||
#RUN apt-get update \
|
||||
# && apt-get install -qq build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
|
||||
#
|
||||
#RUN mkdir -p /opt/node/js \
|
||||
# && cd /opt/node \
|
||||
# && npm i canvas
|
||||
#
|
||||
#WORKDIR /opt/node/js
|
||||
#
|
||||
#ENTRYPOINT ["node"]
|
||||
|
||||
###############
|
||||
# Solution #2 #
|
||||
###############
|
||||
# https://github.com/Automattic/node-canvas/issues/729#issuecomment-352991456
|
||||
# Took 22m 50s
|
||||
|
||||
#FROM ubuntu:xenial
|
||||
#
|
||||
#RUN apt-get update && apt-get install -y \
|
||||
# curl \
|
||||
# git
|
||||
#
|
||||
#RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
|
||||
# && curl -sL https://deb.nodesource.com/setup_8.x | bash - \
|
||||
# && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
# && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
#
|
||||
#RUN apt-get update && apt-get install -y \
|
||||
# nodejs \
|
||||
# yarn \
|
||||
# libcairo2-dev \
|
||||
# libjpeg-dev \
|
||||
# libpango1.0-dev \
|
||||
# libgif-dev \
|
||||
# libpng-dev \
|
||||
# build-essential \
|
||||
# g++
|
||||
|
||||
###############
|
||||
# Solution #3 #
|
||||
###############
|
||||
# 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
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
g++ \
|
||||
cairo-dev \
|
||||
jpeg-dev \
|
||||
pango-dev \
|
||||
bash \
|
||||
imagemagick
|
||||
|
||||
# The rest of the commands to execute
|
||||
|
||||
COPY . .
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
"tsc-watch": "^4.2.9",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.1.2"
|
||||
},
|
||||
"author": "Keanu Timmermans",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from "../../core";
|
||||
import {clean} from "../../lib";
|
||||
import {Config, Storage} from "../../structures";
|
||||
import {Permissions, TextChannel, User} from "discord.js";
|
||||
import {Permissions, TextChannel, User, Role} from "discord.js";
|
||||
import {logs} from "../../modules/globals";
|
||||
|
||||
function getLogBuffer(type: string) {
|
||||
|
@ -162,6 +162,46 @@ export default new NamedCommand({
|
|||
send(`Successfully set this server's stream notifications channel to ${result}.`);
|
||||
}
|
||||
})
|
||||
}),
|
||||
streamrole: new NamedCommand({
|
||||
description: "Sets/removes a stream notification role (and the corresponding category name)",
|
||||
usage: "set/remove <...>",
|
||||
run: "You need to enter in a role.",
|
||||
subcommands: {
|
||||
set: new NamedCommand({
|
||||
usage: "<role> <category>",
|
||||
id: "role",
|
||||
role: new Command({
|
||||
run: "You need to enter a category name.",
|
||||
any: new RestCommand({
|
||||
async run({send, guild, args, combined}) {
|
||||
const role = args[0] as Role;
|
||||
Storage.getGuild(guild!.id).streamingRoles[role.id] = combined;
|
||||
Storage.save();
|
||||
send(
|
||||
`Successfully set the category \`${combined}\` to notify \`${role.name}\`.`
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
remove: new NamedCommand({
|
||||
usage: "<role>",
|
||||
id: "role",
|
||||
role: new Command({
|
||||
async run({send, guild, args}) {
|
||||
const role = args[0] as Role;
|
||||
const guildStorage = Storage.getGuild(guild!.id);
|
||||
const category = guildStorage.streamingRoles[role.id];
|
||||
delete guildStorage.streamingRoles[role.id];
|
||||
Storage.save();
|
||||
send(
|
||||
`Successfully removed the category \`${category}\` to notify \`${role.name}\`.`
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
@ -251,7 +291,7 @@ export default new NamedCommand({
|
|||
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, combined}) {
|
||||
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);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
Command,
|
||||
RestCommand,
|
||||
NamedCommand,
|
||||
CHANNEL_TYPE,
|
||||
getPermissionName,
|
||||
|
@ -51,7 +51,7 @@ export default new NamedCommand({
|
|||
.setColor(EMBED_COLOR);
|
||||
});
|
||||
},
|
||||
any: new Command({
|
||||
any: new RestCommand({
|
||||
async run({send, args}) {
|
||||
const resultingBlob = await getCommandInfo(args);
|
||||
if (typeof resultingBlob === "string") return send(resultingBlob);
|
||||
|
|
|
@ -1,44 +1,142 @@
|
|||
import {NamedCommand, RestCommand} from "../../core";
|
||||
import {streamList} from "../../modules/streamNotifications";
|
||||
import {Storage} from "../../structures";
|
||||
|
||||
// Alternatively, I could make descriptions last outside of just one stream.
|
||||
// But then again, users could just copy paste descriptions. :leaSMUG:
|
||||
// Stream presets (for permanent parts of the description) might come some time in the future.
|
||||
export default new NamedCommand({
|
||||
description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`",
|
||||
async run({send, author, member}) {
|
||||
const userID = author.id;
|
||||
description: "Modifies the current embed for your stream",
|
||||
run: "You need to specify whether to set the description or the image (`desc` and `img` respectively).",
|
||||
subcommands: {
|
||||
description: new NamedCommand({
|
||||
aliases: ["desc"],
|
||||
description:
|
||||
"Sets the description of your stream. You can embed links by writing `[some name](some link)` or remove it",
|
||||
usage: "(<description>)",
|
||||
async run({send, author}) {
|
||||
const userID = author.id;
|
||||
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.description = "No description set.";
|
||||
stream.update();
|
||||
send(`Successfully set the stream description to:`, {
|
||||
embed: {
|
||||
description: "No description set.",
|
||||
color: member!.displayColor
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.description = undefined;
|
||||
stream.update();
|
||||
send("Successfully removed the stream description.");
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Alternatively, I could make descriptions last outside of just one stream.
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, author, member, combined}) {
|
||||
const userID = author.id;
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, author, member, combined}) {
|
||||
const userID = author.id;
|
||||
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.description = combined;
|
||||
stream.update();
|
||||
send(`Successfully set the stream description to:`, {
|
||||
embed: {
|
||||
description: stream.description,
|
||||
color: member!.displayColor
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.description = combined;
|
||||
stream.update();
|
||||
send("Successfully set the stream description to:", {
|
||||
embed: {
|
||||
description: stream.description,
|
||||
color: member!.displayColor
|
||||
}
|
||||
});
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Alternatively, I could make descriptions last outside of just one stream.
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
thumbnail: new NamedCommand({
|
||||
aliases: ["img"],
|
||||
description: "Sets a thumbnail to display alongside the embed or remove it",
|
||||
usage: "(<link>)",
|
||||
async run({send, author}) {
|
||||
const userID = author.id;
|
||||
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.thumbnail = undefined;
|
||||
stream.update();
|
||||
send("Successfully removed the stream thumbnail.");
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, author, member, combined}) {
|
||||
const userID = author.id;
|
||||
|
||||
if (streamList.has(userID)) {
|
||||
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
|
||||
}
|
||||
});
|
||||
} else {
|
||||
send("You can only use this command when streaming.");
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
category: new NamedCommand({
|
||||
aliases: ["cat", "group"],
|
||||
description:
|
||||
"Sets the stream category any future streams will be in (as well as notification roles if set)",
|
||||
usage: "(<category>)",
|
||||
async run({send, guild, author}) {
|
||||
const userID = author.id;
|
||||
const memberStorage = Storage.getGuild(guild!.id).getMember(userID);
|
||||
memberStorage.streamCategory = null;
|
||||
Storage.save();
|
||||
send("Successfully removed the category for all your current and future streams.");
|
||||
|
||||
// Then modify the current category if the user is streaming
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.category = "None";
|
||||
stream.update();
|
||||
}
|
||||
},
|
||||
any: new RestCommand({
|
||||
async run({send, guild, author, combined}) {
|
||||
const userID = author.id;
|
||||
const guildStorage = Storage.getGuild(guild!.id);
|
||||
const memberStorage = guildStorage.getMember(userID);
|
||||
let found = false;
|
||||
|
||||
// Check if it's a valid category
|
||||
for (const [roleID, categoryName] of Object.entries(guildStorage.streamingRoles)) {
|
||||
if (combined === categoryName) {
|
||||
found = true;
|
||||
memberStorage.streamCategory = roleID;
|
||||
Storage.save();
|
||||
send(
|
||||
`Successfully set the category for your current and future streams to: \`${categoryName}\``
|
||||
);
|
||||
|
||||
// Then modify the current category if the user is streaming
|
||||
if (streamList.has(userID)) {
|
||||
const stream = streamList.get(userID)!;
|
||||
stream.category = categoryName;
|
||||
stream.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
send(
|
||||
`No valid category found by \`${combined}\`! The available categories are: \`${Object.values(
|
||||
guildStorage.streamingRoles
|
||||
).join(", ")}\``
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
|
@ -139,12 +139,16 @@ export interface GenericJSON {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
// In order to define a file to write to while also not:
|
||||
// - Using the delete operator (which doesn't work on properties which cannot be undefined)
|
||||
// - Assigning it first then using Object.defineProperty (which raises a flag on CodeQL)
|
||||
// A non-null assertion is used on the class property to say that it'll definitely be assigned.
|
||||
export abstract class GenericStructure {
|
||||
private __meta__ = "generic";
|
||||
private __meta__!: string;
|
||||
|
||||
constructor(tag?: string) {
|
||||
this.__meta__ = tag || this.__meta__;
|
||||
Object.defineProperty(this, "__meta__", {
|
||||
value: tag || "generic",
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Permissions, Message, Collection} from "discord.js";
|
||||
import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Message, Collection} from "discord.js";
|
||||
import {client} from "../index";
|
||||
import {Storage} from "../structures";
|
||||
|
||||
type Stream = {
|
||||
streamer: GuildMember;
|
||||
channel: VoiceChannel;
|
||||
category: string;
|
||||
description?: string;
|
||||
thumbnail?: string;
|
||||
message: Message;
|
||||
streamStart: number;
|
||||
update: () => void;
|
||||
};
|
||||
|
||||
|
@ -14,10 +17,17 @@ type Stream = {
|
|||
export const streamList = new Collection<string, Stream>();
|
||||
|
||||
// Probably find a better, DRY way of doing this.
|
||||
function getStreamEmbed(streamer: GuildMember, channel: VoiceChannel, description?: string): MessageEmbed {
|
||||
function getStreamEmbed(
|
||||
streamer: GuildMember,
|
||||
channel: VoiceChannel,
|
||||
streamStart: number,
|
||||
category: string,
|
||||
description?: string,
|
||||
thumbnail?: string
|
||||
): MessageEmbed {
|
||||
const user = streamer.user;
|
||||
const embed = new MessageEmbed()
|
||||
.setTitle(`Stream: \`#${channel.name}\``)
|
||||
.setTitle(channel.name)
|
||||
.setAuthor(
|
||||
streamer.nickname ?? user.username,
|
||||
user.avatarURL({
|
||||
|
@ -25,11 +35,22 @@ function getStreamEmbed(streamer: GuildMember, channel: VoiceChannel, descriptio
|
|||
format: "png"
|
||||
}) ?? user.defaultAvatarURL
|
||||
)
|
||||
.setColor(streamer.displayColor);
|
||||
// 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("Category", category, true)
|
||||
.setColor(streamer.displayColor)
|
||||
.setFooter(
|
||||
"Stream Started",
|
||||
streamer.guild.iconURL({
|
||||
dynamic: true
|
||||
}) || undefined
|
||||
)
|
||||
.setTimestamp(streamStart);
|
||||
|
||||
if (description) {
|
||||
embed.setDescription(description);
|
||||
}
|
||||
if (description) embed.setDescription(description);
|
||||
if (thumbnail) embed.setThumbnail(thumbnail);
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
@ -40,7 +61,7 @@ client.on("voiceStateUpdate", async (before, after) => {
|
|||
// Note: isStopStreamEvent can be called twice in a row - If Discord crashes/quits while you're streaming, it'll call once with a null channel and a second time with a channel.
|
||||
|
||||
if (isStartStreamEvent || isStopStreamEvent) {
|
||||
const {streamingChannel} = Storage.getGuild(after.guild.id);
|
||||
const {streamingChannel, streamingRoles, members} = Storage.getGuild(after.guild.id);
|
||||
|
||||
if (streamingChannel) {
|
||||
const member = after.member!;
|
||||
|
@ -50,13 +71,42 @@ client.on("voiceStateUpdate", async (before, after) => {
|
|||
// Although checking the bot's permission to send might seem like a good idea, having the error be thrown will cause it to show up in the last channel rather than just show up in the console.
|
||||
if (textChannel instanceof TextChannel) {
|
||||
if (isStartStreamEvent) {
|
||||
const streamStart = Date.now();
|
||||
let streamNotificationPing = "";
|
||||
let category = "None";
|
||||
|
||||
// Check the category if there's one set then ping that role.
|
||||
if (member.id in members) {
|
||||
const roleID = members[member.id].streamCategory;
|
||||
|
||||
// Only continue if they set a valid category.
|
||||
if (roleID && roleID in streamingRoles) {
|
||||
streamNotificationPing = `<@&${roleID}>`;
|
||||
category = streamingRoles[roleID];
|
||||
}
|
||||
}
|
||||
|
||||
streamList.set(member.id, {
|
||||
streamer: member,
|
||||
channel: voiceChannel,
|
||||
message: await textChannel.send(getStreamEmbed(member, voiceChannel)),
|
||||
category,
|
||||
message: await textChannel.send(
|
||||
streamNotificationPing,
|
||||
getStreamEmbed(member, voiceChannel, streamStart, category)
|
||||
),
|
||||
update(this: Stream) {
|
||||
this.message.edit(getStreamEmbed(this.streamer, this.channel, this.description));
|
||||
}
|
||||
this.message.edit(
|
||||
getStreamEmbed(
|
||||
this.streamer,
|
||||
this.channel,
|
||||
streamStart,
|
||||
this.category,
|
||||
this.description,
|
||||
this.thumbnail
|
||||
)
|
||||
);
|
||||
},
|
||||
streamStart
|
||||
});
|
||||
} else if (isStopStreamEvent) {
|
||||
if (streamList.has(member.id)) {
|
||||
|
|
|
@ -57,18 +57,30 @@ class User {
|
|||
}
|
||||
}
|
||||
|
||||
class Member {
|
||||
public streamCategory: string | null;
|
||||
|
||||
constructor(data?: GenericJSON) {
|
||||
this.streamCategory = select(data?.streamCategory, null, String);
|
||||
}
|
||||
}
|
||||
|
||||
class Guild {
|
||||
public prefix: string | null;
|
||||
public welcomeType: "none" | "text" | "graphical";
|
||||
public welcomeChannel: string | null;
|
||||
public welcomeMessage: string | null;
|
||||
public streamingChannel: string | null;
|
||||
public streamingRoles: {[role: string]: string}; // Role ID: Category Name
|
||||
public members: {[id: string]: Member};
|
||||
|
||||
constructor(data?: GenericJSON) {
|
||||
this.prefix = select(data?.prefix, null, String);
|
||||
this.welcomeChannel = select(data?.welcomeChannel, null, String);
|
||||
this.welcomeMessage = select(data?.welcomeMessage, null, String);
|
||||
this.streamingChannel = select(data?.streamingChannel, null, String);
|
||||
this.streamingRoles = {};
|
||||
this.members = {};
|
||||
|
||||
switch (data?.welcomeType) {
|
||||
case "text":
|
||||
|
@ -81,6 +93,37 @@ class Guild {
|
|||
this.welcomeType = "none";
|
||||
break;
|
||||
}
|
||||
|
||||
if (data?.streamingRoles) {
|
||||
for (const id in data.streamingRoles) {
|
||||
const category = data.streamingRoles[id];
|
||||
|
||||
if (/\d{17,}/g.test(id) && typeof category === "string") {
|
||||
this.streamingRoles[id] = category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.members) {
|
||||
for (let id in data.members) {
|
||||
if (/\d{17,}/g.test(id)) {
|
||||
this.members[id] = new Member(data.members[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.`);
|
||||
|
||||
if (id in this.members) return this.members[id];
|
||||
else {
|
||||
const member = new Member();
|
||||
this.members[id] = member;
|
||||
return member;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue