mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Merge branch 'typescript' of https://github.com/keanuplayz/TravBot-v3 into experimental-core-rollout
This commit is contained in:
		
						commit
						9137231768
					
				
					 10 changed files with 365 additions and 3 deletions
				
			
		|  | @ -3,6 +3,7 @@ import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils"; | |||
| import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core"; | ||||
| import {BuyCommand, ShopCommand} from "./modules/eco-shop"; | ||||
| import {MondayCommand} from "./modules/eco-extras"; | ||||
| import {BetCommand} from "./modules/eco-bet"; | ||||
| 
 | ||||
| export default new NamedCommand({ | ||||
|     description: "Economy command for Monika.", | ||||
|  | @ -16,7 +17,8 @@ export default new NamedCommand({ | |||
|         leaderboard: LeaderboardCommand, | ||||
|         buy: BuyCommand, | ||||
|         shop: ShopCommand, | ||||
|         monday: MondayCommand | ||||
|         monday: MondayCommand, | ||||
|         bet: BetCommand | ||||
|     }, | ||||
|     id: "user", | ||||
|     user: new Command({ | ||||
|  |  | |||
							
								
								
									
										198
									
								
								src/commands/fun/modules/eco-bet.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/commands/fun/modules/eco-bet.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| import {Command, NamedCommand, askYesOrNo} from "../../../core"; | ||||
| import {pluralise} from "../../../lib"; | ||||
| import {Storage} from "../../../structures"; | ||||
| import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; | ||||
| import {User} from "discord.js"; | ||||
| 
 | ||||
| export const BetCommand = new NamedCommand({ | ||||
|     description: "Bet your Mons with other people.", | ||||
|     usage: "<user> <amount> <duration>", | ||||
|     run: "Who are you betting with?", | ||||
|     user: new Command({ | ||||
|         description: "User to bet with.", | ||||
|         // handles missing amount argument
 | ||||
|         async run({args, author, channel, guild}) { | ||||
|             if (isAuthorized(guild, channel)) { | ||||
|                 const target = args[0]; | ||||
| 
 | ||||
|                 // handle invalid target
 | ||||
|                 if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); | ||||
|                 else if (target.bot && process.argv[2] !== "dev") return channel.send("You can't bet Mons with a bot!"); | ||||
| 
 | ||||
|                 return channel.send("How much are you betting?"); | ||||
|             } else return; | ||||
|         }, | ||||
|         number: new Command({ | ||||
|             description: "Amount of Mons to bet.", | ||||
|             // handles missing duration argument
 | ||||
|             async run({args, author, channel, guild}) { | ||||
|                 if (isAuthorized(guild, channel)) { | ||||
|                     const sender = Storage.getUser(author.id); | ||||
|                     const target = args[0] as User; | ||||
|                     const receiver = Storage.getUser(target.id); | ||||
|                     const amount = Math.floor(args[1]); | ||||
| 
 | ||||
|                     // handle invalid target
 | ||||
|                     if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); | ||||
|                     else if (target.bot && process.argv[2] !== "dev") | ||||
|                         return channel.send("You can't bet Mons with a bot!"); | ||||
| 
 | ||||
|                     // handle invalid amount
 | ||||
|                     if (amount <= 0) return channel.send("You must bet at least one Mon!"); | ||||
|                     else if (sender.money < amount) | ||||
|                         return channel.send("You don't have enough Mons for that.", getMoneyEmbed(author)); | ||||
|                     else if (receiver.money < amount) | ||||
|                         return channel.send("They don't have enough Mons for that.", getMoneyEmbed(target)); | ||||
| 
 | ||||
|                     return channel.send("How long until the bet ends?"); | ||||
|                 } else return; | ||||
|             }, | ||||
|             any: new Command({ | ||||
|                 description: "Duration of the bet.", | ||||
|                 async run({client, args, author, message, channel, guild}) { | ||||
|                     if (isAuthorized(guild, channel)) { | ||||
|                         // [Pertinence to make configurable on the fly.]
 | ||||
|                         // Lower and upper bounds for bet
 | ||||
|                         const durationBounds = {min: "1m", max: "1d"}; | ||||
| 
 | ||||
|                         const sender = Storage.getUser(author.id); | ||||
|                         const target = args[0] as User; | ||||
|                         const receiver = Storage.getUser(target.id); | ||||
|                         const amount = Math.floor(args[1]); | ||||
|                         const duration = parseDuration(args[2].trim()); | ||||
| 
 | ||||
|                         // handle invalid target
 | ||||
|                         if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); | ||||
|                         else if (target.bot && process.argv[2] !== "dev") | ||||
|                             return channel.send("You can't bet Mons with a bot!"); | ||||
| 
 | ||||
|                         // handle invalid amount
 | ||||
|                         if (amount <= 0) return channel.send("You must bet at least one Mon!"); | ||||
|                         else if (sender.money < amount) | ||||
|                             return channel.send("You don't have enough Mons for that.", getMoneyEmbed(author)); | ||||
|                         else if (receiver.money < amount) | ||||
|                             return channel.send("They don't have enough Mons for that.", getMoneyEmbed(target)); | ||||
| 
 | ||||
|                         // handle invalid duration
 | ||||
|                         if (duration <= 0) return channel.send("Invalid bet duration"); | ||||
|                         else if (duration <= parseDuration(durationBounds.min)) | ||||
|                             return channel.send(`Bet duration is too short, maximum duration is ${durationBounds.min}`); | ||||
|                         else if (duration >= parseDuration(durationBounds.max)) | ||||
|                             return channel.send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); | ||||
| 
 | ||||
|                         // Ask target whether or not they want to take the bet.
 | ||||
|                         const takeBet = await askYesOrNo( | ||||
|                             await channel.send( | ||||
|                                 `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` | ||||
|                             ), | ||||
|                             target.id | ||||
|                         ); | ||||
| 
 | ||||
|                         if (takeBet) { | ||||
|                             // [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
 | ||||
|                             // Remove amount money from both parts at the start to avoid duplication of money.
 | ||||
|                             sender.money -= amount; | ||||
|                             receiver.money -= amount; | ||||
|                             // Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
 | ||||
|                             sender.ecoBetInsurance += amount; | ||||
|                             receiver.ecoBetInsurance += amount; | ||||
|                             Storage.save(); | ||||
| 
 | ||||
|                             // Notify both users.
 | ||||
|                             await channel.send( | ||||
|                                 `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( | ||||
|                                     amount, | ||||
|                                     "Mon", | ||||
|                                     "s" | ||||
|                                 )} has been deducted from each of them.` | ||||
|                             ); | ||||
| 
 | ||||
|                             // Wait for the duration of the bet.
 | ||||
|                             return client.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); | ||||
|                                 // [TODO: when D.JSv13 comes out, inline reply to clean up.]
 | ||||
|                                 // When bet is over, give a vote to ask people their thoughts.
 | ||||
|                                 const voteMsg = await channel.send( | ||||
|                                     `VOTE: do you think that <@${ | ||||
|                                         target.id | ||||
|                                     }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
 | ||||
|                                         message.id | ||||
|                                     }` | ||||
|                                 ); | ||||
|                                 await voteMsg.react("✅"); | ||||
|                                 await voteMsg.react("❌"); | ||||
| 
 | ||||
|                                 // Filter reactions to only collect the pertinent ones.
 | ||||
|                                 voteMsg | ||||
|                                     .awaitReactions( | ||||
|                                         (reaction, user) => { | ||||
|                                             return ["✅", "❌"].includes(reaction.emoji.name); | ||||
|                                         }, | ||||
|                                         // [Pertinence to make configurable on the fly.]
 | ||||
|                                         {time: parseDuration("2m")} | ||||
|                                     ) | ||||
|                                     .then((reactions) => { | ||||
|                                         // Count votes
 | ||||
|                                         const okReaction = reactions.get("✅"); | ||||
|                                         const noReaction = reactions.get("❌"); | ||||
|                                         const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; | ||||
|                                         const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; | ||||
| 
 | ||||
|                                         if (ok > no) { | ||||
|                                             receiver.money += amount * 2; | ||||
|                                             channel.send( | ||||
|                                                 `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` | ||||
|                                             ); | ||||
|                                         } else if (ok < no) { | ||||
|                                             sender.money += amount * 2; | ||||
|                                             channel.send( | ||||
|                                                 `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` | ||||
|                                             ); | ||||
|                                         } else { | ||||
|                                             sender.money += amount; | ||||
|                                             receiver.money += amount; | ||||
|                                             channel.send( | ||||
|                                                 `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` | ||||
|                                             ); | ||||
|                                         } | ||||
|                                         sender.ecoBetInsurance -= amount; | ||||
|                                         receiver.ecoBetInsurance -= amount; | ||||
|                                         Storage.save(); | ||||
|                                     }); | ||||
|                             }, duration); | ||||
|                         } else return await channel.send(`<@${target.id}> has rejected your bet, <@${author.id}>`); | ||||
|                     } else return; | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
|     }) | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * Parses a duration string into milliseconds | ||||
|  * Examples: | ||||
|  * - 3d -> 3 days    -> 259200000ms | ||||
|  * - 2h -> 2 hours   -> 7200000ms | ||||
|  * - 7m -> 7 minutes -> 420000ms | ||||
|  * - 3s -> 3 seconds -> 3000ms | ||||
|  */ | ||||
| function parseDuration(duration: string): number { | ||||
|     // extract last char as unit
 | ||||
|     const unit = duration[duration.length - 1].toLowerCase(); | ||||
|     // get the rest as value
 | ||||
|     let value: number = +duration.substring(0, duration.length - 1); | ||||
| 
 | ||||
|     if (!["d", "h", "m", "s"].includes(unit) || isNaN(value)) return 0; | ||||
| 
 | ||||
|     if (unit === "d") value *= 86400000; | ||||
|     // 1000ms * 60s * 60m * 24h
 | ||||
|     else if (unit === "h") value *= 3600000; | ||||
|     // 1000ms * 60s * 60m
 | ||||
|     else if (unit === "m") value *= 60000; | ||||
|     // 1000ms * 60s
 | ||||
|     else if (unit === "s") value *= 1000; // 1000ms
 | ||||
| 
 | ||||
|     return value; | ||||
| } | ||||
|  | @ -32,6 +32,8 @@ const registry: {[id: string]: string} = { | |||
|         "You are, uhh, Stay Put, Soft Puppy, Es-Pee, Swift Pacemaker, Smug Poyo, and many more.\n...Seriously, this woman has too many names.", | ||||
|     "243061915281129472": | ||||
|         "Some random conlanger, worldbuilder and programmer doofus. ~~May also secretly be a nyan. :3~~", | ||||
|     "792751612904603668": | ||||
|         "Some random nyan. :3 ~~May also secretly be a conlanger, worldbuilder and programmer doofus.~~", | ||||
|     "367439475153829892": "A weeb.", | ||||
|     "760375501775700038": "˙qǝǝʍ ∀", | ||||
|     "389178357302034442": "In his dreams, he is the star. its him. <:itsMe:808174425253871657>", | ||||
|  |  | |||
|  | @ -119,6 +119,32 @@ export default new NamedCommand({ | |||
|                             }) | ||||
|                         }) | ||||
|                     } | ||||
|                 }), | ||||
|                 stream: new NamedCommand({ | ||||
|                     description: "Set a channel to send stream notifications. Type `#` to reference the channel.", | ||||
|                     usage: "(<channel mention>)", | ||||
|                     async run({message, channel, guild, author, member, client, args}) { | ||||
|                         const targetGuild = Storage.getGuild(guild!.id); | ||||
| 
 | ||||
|                         if (targetGuild.streamingChannel) { | ||||
|                             targetGuild.streamingChannel = null; | ||||
|                             channel.send("Removed your server's stream notifications channel."); | ||||
|                         } else { | ||||
|                             targetGuild.streamingChannel = channel.id; | ||||
|                             channel.send(`Set your server's stream notifications channel to ${channel}.`); | ||||
|                         } | ||||
| 
 | ||||
|                         Storage.save(); | ||||
|                     }, | ||||
|                     id: "channel", | ||||
|                     channel: new Command({ | ||||
|                         async run({message, channel, guild, author, member, client, args}) { | ||||
|                             const result = args[0] as TextChannel; | ||||
|                             Storage.getGuild(guild!.id).streamingChannel = result.id; | ||||
|                             Storage.save(); | ||||
|                             channel.send(`Successfully set this server's stream notifications channel to ${result}.`); | ||||
|                         } | ||||
|                     }) | ||||
|                 }) | ||||
|             } | ||||
|         }), | ||||
|  |  | |||
|  | @ -10,7 +10,18 @@ export default new NamedCommand({ | |||
|         let target: Message | undefined; | ||||
|         let distance = 1; | ||||
| 
 | ||||
|         if (args.length >= 2) { | ||||
|         // allows reactions by using an in-line reply
 | ||||
|         if (message.reference) { | ||||
|             const messageID = message.reference.messageID; | ||||
|             try { | ||||
|                 target = await channel.messages.fetch(messageID!); | ||||
|             } catch { | ||||
|                 return channel.send("Unknown error occurred!"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // handles reacts by message id/distance
 | ||||
|         else if (args.length >= 2) { | ||||
|             const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator.
 | ||||
|             const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19}))$/; | ||||
|             const copyIDPattern = /^(?:(\d{17,19})-(\d{17,19}))$/; | ||||
|  |  | |||
							
								
								
									
										18
									
								
								src/commands/utility/streaminfo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/commands/utility/streaminfo.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import {Command, NamedCommand} from "../../core"; | ||||
| import {streamList} from "../../modules/streamNotifications"; | ||||
| 
 | ||||
| export default new NamedCommand({ | ||||
|     description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`", | ||||
|     async run({message, channel, guild, author, member, client, args}) { | ||||
|         const userID = author.id; | ||||
| 
 | ||||
|         if (streamList.has(userID)) { | ||||
|             const stream = streamList.get(userID)!; | ||||
|             stream.description = args.join(" ") || "No description set."; | ||||
|             stream.update(); | ||||
|         } else { | ||||
|             // Alternatively, I could make descriptions last outside of just one stream.
 | ||||
|             channel.send("You can only use this command when streaming."); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | @ -70,3 +70,4 @@ import "./modules/channelListener"; | |||
| import "./modules/intercept"; | ||||
| import "./modules/messageEmbed"; | ||||
| import "./modules/guildMemberAdd"; | ||||
| import "./modules/streamNotifications"; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {client} from "../index"; | ||||
| import {Config} from "../structures"; | ||||
| import {Config, Storage} from "../structures"; | ||||
| 
 | ||||
| client.once("ready", () => { | ||||
|     if (client.user) { | ||||
|  | @ -10,5 +10,21 @@ client.once("ready", () => { | |||
|             type: "LISTENING", | ||||
|             name: `${Config.prefix}help` | ||||
|         }); | ||||
| 
 | ||||
|         // Run this setup block once to restore eco bet money in case the bot went down. (And I guess search the client for those users to let them know too.)
 | ||||
|         for (const id in Storage.users) { | ||||
|             const user = Storage.users[id]; | ||||
| 
 | ||||
|             if (user.ecoBetInsurance > 0) { | ||||
|                 client.users.cache | ||||
|                     .get(id) | ||||
|                     ?.send( | ||||
|                         `Because my system either crashed or restarted while you had a pending bet, the total amount of money that you bet, which was \`${user.ecoBetInsurance}\`, has been restored.` | ||||
|                     ); | ||||
|                 user.money += user.ecoBetInsurance; | ||||
|                 user.ecoBetInsurance = 0; | ||||
|             } | ||||
|         } | ||||
|         Storage.save(); | ||||
|     } | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										84
									
								
								src/modules/streamNotifications.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/modules/streamNotifications.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Permissions, Message, Collection} from "discord.js"; | ||||
| import {client} from "../index"; | ||||
| import {Storage} from "../structures"; | ||||
| 
 | ||||
| type Stream = { | ||||
|     streamer: GuildMember; | ||||
|     channel: VoiceChannel; | ||||
|     description?: string; | ||||
|     message: Message; | ||||
|     update: () => void; | ||||
| }; | ||||
| 
 | ||||
| // A list of user IDs and message embeds.
 | ||||
| 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 { | ||||
|     const user = streamer.user; | ||||
|     const embed = new MessageEmbed() | ||||
|         .setTitle(`Stream: \`#${channel.name}\``) | ||||
|         .setAuthor( | ||||
|             streamer.nickname ?? user.username, | ||||
|             user.avatarURL({ | ||||
|                 dynamic: true, | ||||
|                 format: "png" | ||||
|             }) ?? user.defaultAvatarURL | ||||
|         ) | ||||
|         .setColor(streamer.displayColor); | ||||
| 
 | ||||
|     if (description) { | ||||
|         embed.setDescription(description); | ||||
|     } | ||||
| 
 | ||||
|     return embed; | ||||
| } | ||||
| 
 | ||||
| client.on("voiceStateUpdate", async (before, after) => { | ||||
|     const isStartStreamEvent = !before.streaming && after.streaming; | ||||
|     const isStopStreamEvent = before.streaming && (!after.streaming || !after.channel); // If you were streaming before but now are either not streaming or have left the 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) { | ||||
|         const {streamingChannel} = Storage.getGuild(after.guild.id); | ||||
| 
 | ||||
|         if (streamingChannel) { | ||||
|             const member = after.member!; | ||||
|             const voiceChannel = after.channel!; | ||||
|             const textChannel = client.channels.cache.get(streamingChannel); | ||||
| 
 | ||||
|             if (textChannel instanceof TextChannel) { | ||||
|                 if (isStartStreamEvent) { | ||||
|                     streamList.set(member.id, { | ||||
|                         streamer: member, | ||||
|                         channel: voiceChannel, | ||||
|                         message: await textChannel.send(getStreamEmbed(member, voiceChannel)), | ||||
|                         update(this: Stream) { | ||||
|                             this.message.edit(getStreamEmbed(this.streamer, this.channel, this.description)); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else if (isStopStreamEvent) { | ||||
|                     if (streamList.has(member.id)) { | ||||
|                         const {message} = streamList.get(member.id)!; | ||||
|                         message.delete(); | ||||
|                         streamList.delete(member.id); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 console.error( | ||||
|                     `The streaming notifications channel ${streamingChannel} for guild ${after.guild.id} either doesn't exist or isn't a text channel.` | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| client.on("channelUpdate", (before, after) => { | ||||
|     if (before.type === "voice" && after.type === "voice") { | ||||
|         for (const stream of streamList.values()) { | ||||
|             if (after.id === stream.channel.id) { | ||||
|                 stream.update(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | @ -31,6 +31,7 @@ class User { | |||
|     public timezone: number | null; // This is for the standard timezone only, not the daylight savings timezone
 | ||||
|     public daylightSavingsRegion: "na" | "eu" | "sh" | null; | ||||
|     public todoList: {[timestamp: string]: string}; | ||||
|     public ecoBetInsurance: number; | ||||
| 
 | ||||
|     constructor(data?: GenericJSON) { | ||||
|         this.money = select(data?.money, 0, Number); | ||||
|  | @ -41,6 +42,7 @@ class User { | |||
|             ? data?.daylightSavingsRegion | ||||
|             : null; | ||||
|         this.todoList = {}; | ||||
|         this.ecoBetInsurance = select(data?.ecoBetInsurance, 0, Number); | ||||
| 
 | ||||
|         if (data) { | ||||
|             for (const timestamp in data.todoList) { | ||||
|  | @ -58,11 +60,13 @@ class Guild { | |||
|     public welcomeType: "none" | "text" | "graphical"; | ||||
|     public welcomeChannel: string | null; | ||||
|     public welcomeMessage: string | null; | ||||
|     public streamingChannel: string | null; | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|         switch (data?.welcomeType) { | ||||
|             case "text": | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue