async def cmd_stream_del(self, *, _members: Optional[tuple[discord.Member, ...]] = None): """ ->type Reminders ->signature pg!stream del ->description Remove yourself from the stream-ping-list ->extended description Remove yourself from the stream-ping-list. You can always add you later with `pg!stream add` """ async with db.DiscordDB("stream") as ping_db: data: list = ping_db.get([]) try: if _members: for mem in _members: data.remove(mem.id) else: data.remove(self.author.id) except ValueError: raise BotException( "Could not remove member", "Member was not previously added to the ping list", ) ping_db.write(data) await self.cmd_stream()
async def cmd_stream_ping(self, message: Optional[String] = None): """ ->type Reminders ->signature pg!stream ping [message] ->description Ping users in stream-list with an optional message. ->extended description Ping all users in the ping list to announce a stream. You can pass an optional stream message (like the stream topic). The streamer name will be included and many people will be pinged so \ don't make pranks with this command. """ async with db.DiscordDB("stream") as ping_db: data: list = ping_db.get([]) msg = message.string if message else "Enjoy the stream!" ping = ("Pinging everyone on ping list:\n" + "\n".join( (f"<@!{user}>" for user in data)) if data else "No one is registered on the ping momento :/") try: await self.response_msg.delete() except discord.errors.NotFound: pass await self.channel.send( f"<@!{self.author.id}> is gonna stream!\n{msg}\n{ping}")
async def cmd_stream(self): """ ->type Reminders ->signature pg!stream ->description Show the ping-stream-list Send an embed with all the users currently in the ping-stream-list """ async with db.DiscordDB("stream") as db_obj: data = db_obj.get([]) if not data: await embed_utils.replace( self.response_msg, title="Memento ping list", description="Ping list is empty!", ) return await embed_utils.replace( self.response_msg, title="Memento ping list", description= ("Here is a list of people who want to be pinged when stream starts" "\nUse 'pg!stream ping' to ping them if you start streaming\n" + "\n".join((f"<@{user}>" for user in data))), )
async def cmd_stream_add(self, *, _members: Optional[tuple[discord.Member, ...]] = None): """ ->type Reminders ->signature pg!stream add ->description Add yourself to the stream-ping-list ->extended description Add yourself to the stream-ping-list. You can always delete you later with `pg!stream del` """ async with db.DiscordDB("stream") as ping_db: data: list = ping_db.get([]) if _members: for mem in _members: if mem.id not in data: data.append(mem.id) elif self.author.id not in data: data.append(self.author.id) ping_db.write(data) await self.cmd_stream()
async def cmd_reminders(self): """ ->type Reminders ->signature pg!reminders ->description View all the reminders you have set ----- Implement pg!reminders, for users to view their reminders """ async with db.DiscordDB("reminders") as db_obj: db_data = db_obj.get({}) desc = "You have no reminders set" if self.author.id in db_data: desc = "" cnt = 0 for on, (reminder, chan_id, _) in db_data[self.author.id].items(): channel = None if common.guild is not None: channel = common.guild.get_channel(chan_id) cin = channel.mention if channel is not None else "DM" desc += ( f"Reminder ID: `{cnt}`\n" f"**On {utils.format_datetime(on)} in {cin}:**\n> {reminder}\n\n" ) cnt += 1 await embed_utils.replace( self.response_msg, title=f"Reminders for {self.author.display_name}:", description=desc, )
async def clean_db_member(member: discord.Member): """ This function silently removes users from database messages """ for table_name in ("stream", "reminders", "clock"): async with db.DiscordDB(table_name) as db_obj: data = db_obj.get({}) if member.id in data: data.pop(member) db_obj.write(data)
async def cmd_vibecheck(self): """ ->type Play With Me :snake: ->signature pg!vibecheck ->description Check my mood. ----- Implement pg!vibecheck, to check the snek's emotion """ async with db.DiscordDB("emotions") as db_obj: all_emotions = db_obj.get({}) emotion_percentage = vibecheck.get_emotion_percentage(all_emotions, round_by=-1) all_emotion_response = vibecheck.get_emotion_desc_dict(all_emotions) bot_emotion = max(emotion_percentage.keys(), key=lambda key: emotion_percentage[key]) msg = all_emotion_response[bot_emotion]["msg"] emoji_link = all_emotion_response[bot_emotion]["emoji_link"] if all_emotion_response[bot_emotion].get("override_emotion", None): bot_emotion = all_emotion_response[bot_emotion]["override_emotion"] color = pygame.Color(vibecheck.EMOTION_COLORS[bot_emotion]) t = time.time() pygame.image.save(vibecheck.emotion_pie_chart(all_emotions, 400), f"temp{t}.png") file = discord.File(f"temp{t}.png") try: await self.response_msg.delete() except discord.errors.NotFound: # Message already deleted pass embed_dict = { "title": f"The snek is {bot_emotion} right now!", "description": msg, "thumbnail_url": emoji_link, "footer_text": "This is currently in beta version, so the end product may look different", "footer_icon_url": "https://cdn.discordapp.com/emojis/844513909158969374.png?v=1", "image_url": f"attachment://temp{t}.png", "color": utils.color_to_rgb_int(color), } embed = embed_utils.create(**embed_dict) await self.invoke_msg.reply(file=file, embed=embed, mention_author=False) os.remove(f"temp{t}.png")
async def cmd_reminders_remove(self, *reminder_ids: int): """ ->type Reminders ->signature pg!reminders remove [*ids] ->description Remove reminders ->extended description Remove variable number of reminders, corresponding to each datetime argument The reminder id argument must be an integer If no arguments are passed, the command clears all reminders ->example command pg!reminders remove 1 ----- Implement pg!reminders_remove, for users to remove their reminders """ async with db.DiscordDB("reminders") as db_obj: db_data = db_obj.get({}) db_data_copy = copy.deepcopy(db_data) cnt = 0 if reminder_ids: for reminder_id in sorted(set(reminder_ids), reverse=True): if self.author.id in db_data: for i, dt in enumerate(db_data_copy[self.author.id]): if i == reminder_id: db_data[self.author.id].pop(dt) cnt += 1 break if (reminder_id >= len(db_data_copy[self.author.id]) or reminder_id < 0): raise BotException( "Invalid Reminder ID!", "Reminder ID was not an existing reminder ID", ) if self.author.id in db_data and not db_data[self.author.id]: db_data.pop(self.author.id) elif self.author.id in db_data: cnt = len(db_data.pop(self.author.id)) db_obj.write(db_data) await embed_utils.replace( self.response_msg, title="Reminders removed!", description=f"Successfully removed {cnt} reminder(s)", )
async def get_channel_feature(name: str, channel: common.Channel, defaultret: bool = False): """ Get the channel feature. Returns True if the feature name is disabled on that channel, False otherwise. Also handles category channel """ async with db.DiscordDB("feature") as db_obj: db_dict: dict[int, bool] = db_obj.get({}).get(name, {}) if channel.id in db_dict: return db_dict[channel.id] if isinstance(channel, discord.TextChannel): if channel.category_id is None: return defaultret return db_dict.get(channel.category_id, defaultret) return defaultret
async def routine(): """ Function that gets called routinely. This function inturn, calles other routine functions to handle stuff """ async with db.DiscordDB("reminders") as db_obj: await handle_reminders(db_obj) if random.randint(0, 4) == 0: await emotion.update("bored", 1) await common.bot.change_presence(activity=discord.Activity( type=discord.ActivityType.watching, name="discord.io/pygame_community", )) await asyncio.sleep(3) await common.bot.change_presence(activity=discord.Activity( type=discord.ActivityType.playing, name="in discord.io/pygame_community", ))
async def cmd_clock( self, action: str = "", timezone: Optional[float] = None, color: Optional[pygame.Color] = None, *, _member: Optional[discord.Member] = None, ): """ ->type Get help ->signature pg!clock ->description 24 Hour Clock showing <@&778205389942030377> s who are available to help -> Extended description People on the clock can run the clock with more arguments, to update their data. `pg!clock update [timezone in hours] [color as hex string]` `timezone` is float offset from GMT in hours. `color` optional color argument, that shows up on the clock. Note that you might not always display with that colour. This happens if more than one person are on the same timezone Use `pg!clock remove` to remove yourself from the clock ----- Implement pg!clock, to display a clock of helpfulies/mods/wizards """ async with db.DiscordDB("clock") as db_obj: timezones = db_obj.get({}) if action: if _member is None: member = self.author if member.id not in timezones: raise BotException( "Cannot update clock!", "You cannot run clock update commands because you are " + "not on the clock", ) else: member = _member if action == "update": if timezone is not None and abs(timezone) > 12: raise BotException("Failed to update clock!", "Timezone offset out of range") if member.id in timezones: if timezone is not None: timezones[member.id][0] = timezone if color is not None: timezones[member.id][1] = utils.color_to_rgb_int( color) else: if timezone is None: raise BotException( "Failed to update clock!", "Timezone is required when adding new people", ) if color is None: color = pygame.Color(( random.randint(0, 0xFFFFFF) << 8) | 0xFF) timezones[member.id] = [ timezone, utils.color_to_rgb_int(color) ] # sort timezones dict after an update operation timezones = dict( sorted(timezones.items(), key=lambda x: x[1][0])) elif action == "remove": try: timezones.pop(member.id) except KeyError: raise BotException( "Failed to update clock!", "Cannot remove non-existing person from clock", ) else: raise BotException("Failed to update clock!", f"Invalid action specifier {action}") db_obj.write(timezones) t = time.time() pygame.image.save( await clock.user_clock(t, timezones, self.get_guild()), f"temp{t}.png") common.cmd_logs[self.invoke_msg.id] = await self.channel.send( file=discord.File(f"temp{t}.png")) os.remove(f"temp{t}.png") try: await self.response_msg.delete() except discord.NotFound: pass
async def cmd_reminders_add( self, msg: String, on: datetime.datetime, *, _delta: Optional[datetime.timedelta] = None, ): """ ->type Reminders ->signature pg!reminders add <message> <datetime in iso format> ->description Set a reminder to yourself ->extended description Allows you to set a reminder to yourself The date-time must be an ISO time formatted string, in UTC time string ->example command pg!reminders add "do the thing" "2034-10-26 11:19:36" ----- Implement pg!reminders_add, for users to set reminders for themselves """ if _delta is None: now = datetime.datetime.utcnow() _delta = on - now else: now = on on = now + _delta if on < now: raise BotException( "Failed to set reminder!", "Time cannot go backwards, negative time does not make sense..." "\n Or does it? \\*vsauce music plays in the background\\*", ) elif _delta <= datetime.timedelta(seconds=10): raise BotException( "Failed to set reminder!", "Why do you want me to set a reminder for such a small duration?\n" "Pretty sure you can remember that one yourself :wink:", ) # remove microsecond precision of the 'on' variable on -= datetime.timedelta(microseconds=on.microsecond) async with db.DiscordDB("reminders") as db_obj: db_data = db_obj.get({}) if self.author.id not in db_data: db_data[self.author.id] = {} # user is editing old reminder message, discard the old reminder for key, (_, chan_id, msg_id) in tuple(db_data[self.author.id].items()): if chan_id == self.channel.id and msg_id == self.invoke_msg.id: db_data[self.author.id].pop(key) limit = 25 if self.is_priv else 10 if len(db_data[self.author.id]) >= limit: raise BotException( "Failed to set reminder!", f"I cannot set more than {limit} reminders for you", ) db_data[self.author.id][on] = ( msg.string.strip(), self.channel.id, self.invoke_msg.id, ) db_obj.write(db_data) await embed_utils.replace( self.response_msg, title="Reminder set!", description= (f"Gonna remind {self.author.name} in {utils.format_timedelta(_delta)}\n" f"And that is on {utils.format_datetime(on)}"), )
async def call_cmd(self): """ Command handler, calls the appropriate sub function to handle commands. This one takes in the parsed arguments from the parse_args function, and handles that according to the function being called, by using the inspect module, and verifying that all args and kwargs are accurate before calling the actual function. Relies on argument annotations to cast args/kwargs to the types required by the function """ cmd, args, kwargs = parse_args(self.cmd_str) # command has been blacklisted from running async with db.DiscordDB("blacklist") as db_obj: if cmd in db_obj.get([]): raise BotException( "Cannot execute comamand!", f"The command '{cmd}' has been temporarily been blocked from " "running, while wizards are casting their spells on it!\n" "Please try running the command after the maintenance work " "has been finished", ) # First check if it is a group command, and handle it. # get the func object is_group = False func = None if cmd in self.groups: # iterate over group commands sorted in descending order, so that # we find the correct match for func in sorted(self.groups[cmd], key=lambda x: len(x.subcmds), reverse=True): n = len(func.subcmds) if func.subcmds == tuple(args[:n]): args = args[n:] is_group = True break if not is_group: if cmd not in self.cmds_and_funcs: if cmd in common.admin_commands: raise BotException( "Permissions Error!", f"The command '{cmd}' is an admin command, and you do " "not have access to that", ) raise BotException( "Unrecognized command!", f"The command '{cmd}' does not exist.\nFor help on bot " "commands, do `pg!help`", ) func = self.cmds_and_funcs[cmd] if hasattr(func, "no_dm") and self.is_dm: raise BotException( "Cannot run this commands on DM", "This command is not supported on DMs", ) if hasattr(func, "fun_cmd"): if await utils.get_channel_feature("nofun", self.channel): raise BotException( "Could not run command!", "This command is a 'fun' command, and is not allowed " "in this channel. Please try running the command in " "some other channel.", ) bored = await emotion.get("bored") if bored < -60 and -bored / 100 >= random.random(): raise BotException( "I am Exhausted!", "I have been running a lot of commands lately, and now I am tired.\n" "Give me a bit of a break, and I will be back to normal!", ) confused = await emotion.get("confused") if confused > 60 and random.random() < confused / 400: await embed_utils.replace( self.response_msg, title="I am confused...", description="Hang on, give me a sec...", ) await asyncio.sleep(random.randint(3, 5)) await embed_utils.replace( self.response_msg, title="Oh, never mind...", description="Sorry, I was confused for a sec there", ) await asyncio.sleep(0.5) if func is None: raise BotException("Internal bot error", "This should never happen kek") # If user has put an attachment, check whether it's a text file, and # handle as code block for attach in self.invoke_msg.attachments: if attach.content_type is not None and ( attach.content_type.startswith("text") or attach.content_type.endswith(("json", "javascript"))): contents = await attach.read() ext = "" if "." in attach.filename: ext = attach.filename.split(".")[-1] args.append(CodeBlock(contents.decode(), ext)) sig = inspect.signature(func) i = -1 is_var_pos = is_var_key = False keyword_only_args = [] all_keywords = [] # iterate through function parameters, arrange the given args and # kwargs in the order and format the function wants for i, key in enumerate(sig.parameters): param = sig.parameters[key] iskw = False if param.kind not in [param.POSITIONAL_ONLY, param.VAR_POSITIONAL]: all_keywords.append(key) if (i == 0 and isinstance(param.annotation, str) and self.invoke_msg.reference is not None and ("discord.Message" in param.annotation or "discord.PartialMessage" in param.annotation)): # first arg is expected to be a Message object, handle reply into # the first argument msg = str(self.invoke_msg.reference.message_id) if self.invoke_msg.reference.channel_id != self.channel.id: msg = str(self.invoke_msg.reference.channel_id) + "/" + msg args.insert(0, msg) if param.kind == param.VAR_POSITIONAL: is_var_pos = True for j in range(i, len(args)): args[j] = await self.cast_arg(param, args[j], cmd) continue elif param.kind == param.VAR_KEYWORD: is_var_key = True for j in kwargs: if j not in keyword_only_args: kwargs[j] = await self.cast_arg(param, kwargs[j], cmd) continue elif param.kind == param.KEYWORD_ONLY: iskw = True keyword_only_args.append(key) if key not in kwargs: if param.default == param.empty: raise KwargError( f"Missed required keyword argument `{key}`", cmd) kwargs[key] = param.default continue elif i == len(args): # ran out of args, try to fill it with something if key in kwargs: if param.kind == param.POSITIONAL_ONLY: raise ArgError( f"`{key}` cannot be passed as a keyword argument", cmd) args.append(kwargs.pop(key)) elif param.default == param.empty: raise ArgError(f"Missed required argument `{key}`", cmd) else: args.append(param.default) continue elif key in kwargs: raise ArgError( "Positional cannot be passed again as a keyword argument", cmd) # cast the argument into the required type if iskw: kwargs[key] = await self.cast_arg(param, kwargs[key], cmd, key) else: args[i] = await self.cast_arg(param, args[i], cmd, key) i += 1 # More arguments were given than required if not is_var_pos and i < len(args): raise ArgError(f"Too many args were given (`{len(args)}`)", cmd) # Iterate through kwargs to check if we received invalid ones if not is_var_key: for key in kwargs: if key not in all_keywords: raise KwargError( f"Received invalid keyword argument `{key}`", cmd) await func(*args, **kwargs)