mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Improved stream notifications
This commit is contained in:
		
							parent
							
								
									728f115de9
								
							
						
					
					
						commit
						9bf44c160a
					
				
					 4 changed files with 279 additions and 48 deletions
				
			
		|  | @ -9,7 +9,7 @@ import { | ||||||
| } from "../../core"; | } from "../../core"; | ||||||
| import {clean} from "../../lib"; | import {clean} from "../../lib"; | ||||||
| import {Config, Storage} from "../../structures"; | 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"; | import {logs} from "../../modules/globals"; | ||||||
| 
 | 
 | ||||||
| function getLogBuffer(type: string) { | function getLogBuffer(type: string) { | ||||||
|  | @ -162,6 +162,46 @@ export default new NamedCommand({ | ||||||
|                             send(`Successfully set this server's stream notifications channel to ${result}.`); |                             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.", |             run: "You have to enter some code to execute first.", | ||||||
|             any: new RestCommand({ |             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.
 |                 // 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 { |                     try { | ||||||
|                         let evaled = eval(combined); |                         let evaled = eval(combined); | ||||||
|                         if (typeof evaled !== "string") evaled = require("util").inspect(evaled); |                         if (typeof evaled !== "string") evaled = require("util").inspect(evaled); | ||||||
|  |  | ||||||
|  | @ -1,23 +1,28 @@ | ||||||
| import {NamedCommand, RestCommand} from "../../core"; | import {NamedCommand, RestCommand} from "../../core"; | ||||||
| import {streamList} from "../../modules/streamNotifications"; | 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({ | export default new NamedCommand({ | ||||||
|     description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`", |     description: "Modifies the current embed for your stream", | ||||||
|     async run({send, author, member}) { |     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; |                 const userID = author.id; | ||||||
| 
 | 
 | ||||||
|                 if (streamList.has(userID)) { |                 if (streamList.has(userID)) { | ||||||
|                     const stream = streamList.get(userID)!; |                     const stream = streamList.get(userID)!; | ||||||
|             stream.description = "No description set."; |                     stream.description = undefined; | ||||||
|                     stream.update(); |                     stream.update(); | ||||||
|             send(`Successfully set the stream description to:`, { |                     send("Successfully removed the stream description."); | ||||||
|                 embed: { |  | ||||||
|                     description: "No description set.", |  | ||||||
|                     color: member!.displayColor |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|                 } else { |                 } else { | ||||||
|             // Alternatively, I could make descriptions last outside of just one stream.
 |  | ||||||
|                     send("You can only use this command when streaming."); |                     send("You can only use this command when streaming."); | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  | @ -29,16 +34,109 @@ export default new NamedCommand({ | ||||||
|                         const stream = streamList.get(userID)!; |                         const stream = streamList.get(userID)!; | ||||||
|                         stream.description = combined; |                         stream.description = combined; | ||||||
|                         stream.update(); |                         stream.update(); | ||||||
|                 send(`Successfully set the stream description to:`, { |                         send("Successfully set the stream description to:", { | ||||||
|                             embed: { |                             embed: { | ||||||
|                                 description: stream.description, |                                 description: stream.description, | ||||||
|                                 color: member!.displayColor |                                 color: member!.displayColor | ||||||
|                             } |                             } | ||||||
|                         }); |                         }); | ||||||
|                     } else { |                     } else { | ||||||
|                 // Alternatively, I could make descriptions last outside of just one stream.
 |  | ||||||
|                         send("You can only use this command when streaming."); |                         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(", ")}\`` | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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 {client} from "../index"; | ||||||
| import {Storage} from "../structures"; | import {Storage} from "../structures"; | ||||||
| 
 | 
 | ||||||
| type Stream = { | type Stream = { | ||||||
|     streamer: GuildMember; |     streamer: GuildMember; | ||||||
|     channel: VoiceChannel; |     channel: VoiceChannel; | ||||||
|  |     category: string; | ||||||
|     description?: string; |     description?: string; | ||||||
|  |     thumbnail?: string; | ||||||
|     message: Message; |     message: Message; | ||||||
|  |     streamStart: number; | ||||||
|     update: () => void; |     update: () => void; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -14,10 +17,17 @@ type Stream = { | ||||||
| export const streamList = new Collection<string, Stream>(); | export const streamList = new Collection<string, Stream>(); | ||||||
| 
 | 
 | ||||||
| // Probably find a better, DRY way of doing this.
 | // 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 user = streamer.user; | ||||||
|     const embed = new MessageEmbed() |     const embed = new MessageEmbed() | ||||||
|         .setTitle(`Stream: \`#${channel.name}\``) |         .setTitle(channel.name) | ||||||
|         .setAuthor( |         .setAuthor( | ||||||
|             streamer.nickname ?? user.username, |             streamer.nickname ?? user.username, | ||||||
|             user.avatarURL({ |             user.avatarURL({ | ||||||
|  | @ -25,11 +35,22 @@ function getStreamEmbed(streamer: GuildMember, channel: VoiceChannel, descriptio | ||||||
|                 format: "png" |                 format: "png" | ||||||
|             }) ?? user.defaultAvatarURL |             }) ?? 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) { |     if (description) embed.setDescription(description); | ||||||
|         embed.setDescription(description); |     if (thumbnail) embed.setThumbnail(thumbnail); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     return embed; |     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.
 |     // 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) { |     if (isStartStreamEvent || isStopStreamEvent) { | ||||||
|         const {streamingChannel} = Storage.getGuild(after.guild.id); |         const {streamingChannel, streamingRoles, members} = Storage.getGuild(after.guild.id); | ||||||
| 
 | 
 | ||||||
|         if (streamingChannel) { |         if (streamingChannel) { | ||||||
|             const member = after.member!; |             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.
 |             // 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 (textChannel instanceof TextChannel) { | ||||||
|                 if (isStartStreamEvent) { |                 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, { |                     streamList.set(member.id, { | ||||||
|                         streamer: member, |                         streamer: member, | ||||||
|                         channel: voiceChannel, |                         channel: voiceChannel, | ||||||
|                         message: await textChannel.send(getStreamEmbed(member, voiceChannel)), |                         category, | ||||||
|  |                         message: await textChannel.send( | ||||||
|  |                             streamNotificationPing, | ||||||
|  |                             getStreamEmbed(member, voiceChannel, streamStart, category) | ||||||
|  |                         ), | ||||||
|                         update(this: Stream) { |                         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) { |                 } else if (isStopStreamEvent) { | ||||||
|                     if (streamList.has(member.id)) { |                     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 { | class Guild { | ||||||
|     public prefix: string | null; |     public prefix: string | null; | ||||||
|     public welcomeType: "none" | "text" | "graphical"; |     public welcomeType: "none" | "text" | "graphical"; | ||||||
|     public welcomeChannel: string | null; |     public welcomeChannel: string | null; | ||||||
|     public welcomeMessage: string | null; |     public welcomeMessage: string | null; | ||||||
|     public streamingChannel: string | null; |     public streamingChannel: string | null; | ||||||
|  |     public streamingRoles: {[role: string]: string}; // Role ID: Category Name
 | ||||||
|  |     public members: {[id: string]: Member}; | ||||||
| 
 | 
 | ||||||
|     constructor(data?: GenericJSON) { |     constructor(data?: GenericJSON) { | ||||||
|         this.prefix = select(data?.prefix, null, String); |         this.prefix = select(data?.prefix, null, String); | ||||||
|         this.welcomeChannel = select(data?.welcomeChannel, null, String); |         this.welcomeChannel = select(data?.welcomeChannel, null, String); | ||||||
|         this.welcomeMessage = select(data?.welcomeMessage, null, String); |         this.welcomeMessage = select(data?.welcomeMessage, null, String); | ||||||
|         this.streamingChannel = select(data?.streamingChannel, null, String); |         this.streamingChannel = select(data?.streamingChannel, null, String); | ||||||
|  |         this.streamingRoles = {}; | ||||||
|  |         this.members = {}; | ||||||
| 
 | 
 | ||||||
|         switch (data?.welcomeType) { |         switch (data?.welcomeType) { | ||||||
|             case "text": |             case "text": | ||||||
|  | @ -81,6 +93,37 @@ class Guild { | ||||||
|                 this.welcomeType = "none"; |                 this.welcomeType = "none"; | ||||||
|                 break; |                 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue