Improved stream notifications

This commit is contained in:
WatDuhHekBro 2021-04-12 12:43:13 -05:00
parent 728f115de9
commit 9bf44c160a
4 changed files with 279 additions and 48 deletions

View File

@ -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);

View File

@ -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(", ")}\``
);
}
}
})
})
}
});

View File

@ -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)) {

View File

@ -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;
}
}
}