# Command handlers # Incoming commands are fully handled by functions defined here. from common import BotVersion from textwrap import dedent import discord import pytz from datetime import datetime import subprocess from userdb import UserDatabase from common import tzlcmap, logPrint class WtCommands: def __init__(self, userdb: UserDatabase, client: discord.Client): self.userdb = userdb self.dclient = client self.commandlist = { 'help' : self.cmd_help, 'list' : self.cmd_list, 'time' : self.cmd_time, 'set' : self.cmd_set, 'remove': self.cmd_remove } async def dispatch(self, cmdBase: str, message: discord.Message): try: command = self.commandlist[cmdBase] except KeyError: return logPrint('Command invoked', '{0}/{1}: tz.{2}'.format(message.guild, message.author, cmdBase)) await command(message.guild, message.channel, message.author, message.content) # ------ # Helper methods def _tzPrint(self, zone : str): """ Returns a string displaying the current time in the given time zone. Resulting string should be placed in a code block. """ padding = '' now_time = datetime.now(pytz.timezone(zone)) if len(now_time.strftime("%Z")) != 4: padding = ' ' return "{:s}{:s} | {:s}".format(now_time.strftime("%H:%M %d-%b %Z%z"), padding, zone) def _userResolve(self, guild: discord.Guild, userIds: list): """ Given a list with user IDs, returns a string, the second half of a list entry, describing the users for which a zone is represented by. """ if len(userIds) == 0: return " → This text should never appear." # Try given entries. For each entry tried, attempt to get their nickname # or username. Failed attempts are anonymized instead of discarded. namesProcessed = 0 namesSkipped = 0 processedNames = [] while namesProcessed < 4 and len(userIds) > 0: namesProcessed += 1 uid = userIds.pop() mem = guild.get_member(int(uid)) if mem is not None: processedNames.append(mem.display_name) else: namesSkipped += 1 leftovers = namesSkipped + len(userIds) if len(processedNames) == 0: return " → {0} user{1}.".format(leftovers, "s" if leftovers != 1 else "") result = " → " while len(processedNames) > 0: result += processedNames.pop() + ", " if leftovers != 0: result += "{0} other{1}.".format(leftovers, "s" if leftovers != 1 else "") else: result = result[:-2] + "." return result # ------ # Individual command handlers # All command functions are expected to have this signature: # def cmd_NAME(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str) async def cmd_help(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str): # Be a little fancy. tzcount = self.userdb.get_unique_tz_count() em = discord.Embed( color=14742263, title='Help & About', description=dedent(''' World Time v`{0}` Serving {1} communities across {2} time zones. '''.format(BotVersion, len(self.dclient.guilds), tzcount)) ) em.set_footer(text='World Time', icon_url=self.dclient.user.avatar_url) em.add_field(name='Commands', value=dedent(''' `tz.help` - This message. `tz.list` - Displays current times for all recently active known users. `tz.list [user]` - Displays the current time for the given *user*. `tz.time` - Displays the current time in your time zone. `tz.time [zone]` - Displays the current time in the given *zone*. `tz.set [zone]` - Registers or updates your *zone* with the bot. `tz.remove` - Removes your name from this bot. ''')) em.add_field(name='Zones', value=dedent(''' This bot uses zone names from the tz database. Most common zones are supported. For a list of entries, see the "TZ database name" column under https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. ''')) await channel.send(embed=em) async def cmd_time(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str): wspl = msgcontent.split(' ', 1) if len(wspl) == 1: # No parameter. So, doing the same thing anyway... await self._list_userparam(guild, channel, author, author.id) else: try: zoneinput = tzlcmap[wspl[1].lower()] except KeyError: await channel.send(':x: Not a valid zone name.') return resultstr = '```\n' + self._tzPrint(zoneinput) + '\n```' await channel.send(resultstr) async def cmd_set(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str): wspl = msgcontent.split(' ', 1) if len(wspl) == 1: # No parameter. But it's required await channel.send(':x: Zone parameter is required.') return try: zoneinput = tzlcmap[wspl[1].lower()] except KeyError: await channel.send(':x: Not a valid zone name.') return self.userdb.update_user(guild.id, author.id, zoneinput) await channel.send(':white_check_mark: Your zone has been set.') async def cmd_list(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str): wspl = msgcontent.split(' ', 1) if len(wspl) == 1: await self._list_noparam2(guild, channel) else: await self._list_userparam(guild, channel, author, wspl[1]) async def cmd_remove(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str): # To do: Check if there even is data to remove; react accordingly self.userdb.delete_user(guild.id, author.id) await channel.send(':white_check_mark: Your zone has been removed.') # ------ # Supplemental command functions async def _list_noparam(self, guild: discord.Guild, channel: discord.TextChannel): # To do: improve and merge into noparam2 clist = self.userdb.get_list(guild.id) if len(clist) == 0: await channel.send(':x: No users with registered time zones have been active in the last 30 days.') return resultarr = [] for i in clist: resultarr.append(self._tzPrint(i)) resultarr.sort() resultstr = '```\n' for i in resultarr: resultstr += i + '\n' resultstr += '```' await channel.send(resultstr) async def _list_noparam2(self, guild: discord.Guild, channel: discord.TextChannel): rawlist = self.userdb.get_list2(guild.id) if len(rawlist) == 0: await channel.send(':x: No users with registered time zones have been active in the last 30 days.') return if guild.large: # Get full user data here if not available (used by _userResolve) await self.dclient.request_offline_members(guild) resultData = [] for key, value in rawlist.items(): resultData.append(self._tzPrint(key) + '\n' + self._userResolve(guild, value)) resultData.sort() resultFinal = '```\n' for i in resultData: resultFinal += i + '\n' resultFinal += '```' await channel.send(resultFinal) async def _list_userparam(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, param): # wishlist: search based on username/nickname param = str(param) usersearch = self._resolve_user(guild, param) if usersearch is None: await channel.send(':x: Cannot find the specified user.') return res = self.userdb.get_list(guild.id, usersearch.id) if len(res) == 0: ownsearch = author.id == param if ownsearch: await channel.send(':x: You do not have a time zone. Set it with `tz.set`.') else: await channel.send(':x: The given user does not have a time zone set.') return resultstr = '```\n' + self._tzPrint(res[0]) + '\n```' await channel.send(resultstr) def _resolve_user(self, guild: discord.Guild, inputstr: str): """ Takes a string input and attempts to find the corresponding user. """ idsearch = None try: idsearch = int(inputstr) except ValueError: pass if inputstr.startswith('<@!') and inputstr.endswith('>'): idsearch = inputstr[3:][:-1] if inputstr.startswith('<@') and inputstr.endswith('>'): idsearch = inputstr[2:][:-1] if idsearch is not None: return guild.get_member(idsearch) # get_member_named is case-sensitive. we do it ourselves. username only. for member in guild.members: # we'll use the discriminator and do a username lookup if it exists if len(inputstr) > 5 and inputstr[-5] == '#': discstr = inputstr[-4:] userstr = inputstr[:-5] if discstr.isdigit(): if member.discriminator == discstr and userstr.lower() == member.name.lower(): return member #nickname search if member.nick is not None: if member.nick.lower() == inputstr.lower(): return member #username search if member.name.lower() == inputstr.lower(): return member return None