2018-08-24 20:27:56 +00:00
|
|
|
# Command handlers
|
2019-12-27 03:33:40 +00:00
|
|
|
# Incoming commands are fully handled by functions defined here.
|
2018-08-24 20:27:56 +00:00
|
|
|
|
2020-03-01 22:47:51 +00:00
|
|
|
from common import BotVersion, tzPrint
|
2018-08-24 20:27:56 +00:00
|
|
|
from textwrap import dedent
|
|
|
|
import discord
|
2018-08-28 18:17:22 +00:00
|
|
|
import pytz
|
|
|
|
from datetime import datetime
|
2019-12-27 03:51:32 +00:00
|
|
|
import re
|
2020-03-01 22:47:51 +00:00
|
|
|
import collections
|
2018-08-24 20:27:56 +00:00
|
|
|
|
|
|
|
from userdb import UserDatabase
|
2018-08-28 18:17:22 +00:00
|
|
|
from common import tzlcmap, logPrint
|
2018-08-24 20:27:56 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
'set' : self.cmd_set,
|
2019-12-27 03:51:32 +00:00
|
|
|
'remove': self.cmd_remove,
|
|
|
|
'setfor': self.cmd_setFor,
|
|
|
|
'removefor': self.cmd_removeFor
|
2018-08-24 20:27:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async def dispatch(self, cmdBase: str, message: discord.Message):
|
|
|
|
try:
|
|
|
|
command = self.commandlist[cmdBase]
|
|
|
|
except KeyError:
|
|
|
|
return
|
2020-02-19 17:49:18 +00:00
|
|
|
logPrint('Command invoked', '{0}/{1}: {2}'.format(message.guild, message.author, message.content))
|
2018-08-24 20:27:56 +00:00
|
|
|
await command(message.guild, message.channel, message.author, message.content)
|
|
|
|
|
2018-09-01 19:41:44 +00:00
|
|
|
# ------
|
|
|
|
# Helper methods
|
2020-03-01 21:45:41 +00:00
|
|
|
|
|
|
|
def _userFormat(self, member: discord.Member):
|
|
|
|
"""
|
|
|
|
Given a member, returns a formatted string showing their username and nickname
|
|
|
|
prepared for result output.
|
|
|
|
"""
|
|
|
|
username = self._userFormatEscapeFormattingCharacters(member.name)
|
|
|
|
if member.nick is not None:
|
|
|
|
nickname = self._userFormatEscapeFormattingCharacters(member.nick)
|
|
|
|
return "**{}** ({}#{})".format(nickname, username, member.discriminator)
|
|
|
|
else:
|
|
|
|
return "**{}#{}**".format(username, member.discriminator)
|
|
|
|
|
|
|
|
def _userFormatEscapeFormattingCharacters(self, input: str):
|
|
|
|
result = ''
|
|
|
|
for char in input:
|
|
|
|
if char == '\\' or char == '_' or char == '~' or char == '*':
|
|
|
|
result += '\\'
|
|
|
|
result += char
|
|
|
|
return result
|
2018-09-01 19:41:44 +00:00
|
|
|
|
2019-12-27 03:51:32 +00:00
|
|
|
def _isUserAdmin(self, member: discord.Member):
|
|
|
|
"""
|
|
|
|
Checks if the given user can be considered a guild admin ('Manage Server' is set).
|
|
|
|
"""
|
|
|
|
# Can fit in a BirthdayBot-like bot moderator role in here later, if desired.
|
|
|
|
p = member.guild_permissions
|
|
|
|
return p.administrator or p.manage_guild
|
|
|
|
|
|
|
|
def _resolveUserParam(self, guild: discord.Guild, input: str):
|
|
|
|
"""
|
|
|
|
Given user input, attempts to find the corresponding Member.
|
|
|
|
Currently only accepts pings and explicit user IDs.
|
|
|
|
"""
|
|
|
|
UserMention = re.compile(r"<@\!?(\d+)>")
|
|
|
|
match = UserMention.match(input)
|
|
|
|
if match is not None:
|
|
|
|
idcheck = match.group(1)
|
|
|
|
else:
|
|
|
|
idcheck = input
|
|
|
|
try:
|
|
|
|
idcheck = int(idcheck)
|
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
return guild.get_member(idcheck)
|
|
|
|
|
2018-08-24 20:27:56 +00:00
|
|
|
# ------
|
|
|
|
# Individual command handlers
|
2018-09-01 19:41:44 +00:00
|
|
|
# All command functions are expected to have this signature:
|
|
|
|
# def cmd_NAME(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str)
|
2018-08-24 20:27:56 +00:00
|
|
|
|
|
|
|
async def cmd_help(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str):
|
2018-09-01 20:26:47 +00:00
|
|
|
# Be a little fancy.
|
|
|
|
tzcount = self.userdb.get_unique_tz_count()
|
|
|
|
|
2018-08-24 20:27:56 +00:00
|
|
|
em = discord.Embed(
|
|
|
|
color=14742263,
|
|
|
|
title='Help & About',
|
2018-09-01 20:26:47 +00:00
|
|
|
description=dedent('''
|
2019-12-27 04:17:39 +00:00
|
|
|
World Time v{0}
|
2018-09-01 20:26:47 +00:00
|
|
|
Serving {1} communities across {2} time zones.
|
2019-12-27 03:33:40 +00:00
|
|
|
'''.format(BotVersion, len(self.dclient.guilds), tzcount))
|
2018-09-01 20:26:47 +00:00
|
|
|
)
|
2018-08-24 20:27:56 +00:00
|
|
|
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.set [zone]` - Registers or updates your *zone* with the bot.
|
|
|
|
`tz.remove` - Removes your name from this bot.
|
|
|
|
'''))
|
2019-12-27 04:17:39 +00:00
|
|
|
em.add_field(name='Admin commands', value=dedent('''
|
|
|
|
`tz.setFor [user] [zone]` - Sets the time zone for another user.
|
|
|
|
`tz.removeFor [user]` - Removes another user's information.
|
|
|
|
'''), inline=False)
|
2018-08-24 20:27:56 +00:00
|
|
|
em.add_field(name='Zones', value=dedent('''
|
2019-09-08 03:09:58 +00:00
|
|
|
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.
|
2019-12-27 03:51:32 +00:00
|
|
|
'''), inline=False)
|
2018-08-24 20:27:56 +00:00
|
|
|
await channel.send(embed=em)
|
|
|
|
|
|
|
|
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.')
|
|
|
|
|
2019-12-27 03:51:32 +00:00
|
|
|
async def cmd_setFor(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str):
|
|
|
|
if not self._isUserAdmin(author):
|
|
|
|
# Silently ignore
|
|
|
|
return
|
|
|
|
|
|
|
|
# parameters: command, target, zone
|
|
|
|
wspl = msgcontent.split(' ', 2)
|
|
|
|
|
|
|
|
if len(wspl) == 1:
|
|
|
|
await channel.send(":x: You must specify a user to set the time zone for.")
|
|
|
|
return
|
|
|
|
if len(wspl) == 2:
|
|
|
|
await channel.send(":x: You must specify a time zone to apply to the user.")
|
|
|
|
return
|
|
|
|
|
|
|
|
# Determine user from second parameter
|
|
|
|
targetuser = self._resolveUserParam(guild, wspl[1])
|
|
|
|
if targetuser is None:
|
|
|
|
await channel.send(":x: Unable to find the target user.")
|
|
|
|
return
|
|
|
|
|
|
|
|
# Check the third parameter
|
|
|
|
try:
|
|
|
|
zoneinput = tzlcmap[wspl[2].lower()]
|
|
|
|
except KeyError:
|
|
|
|
await channel.send(':x: Not a valid zone name.')
|
|
|
|
return
|
|
|
|
|
|
|
|
# Do the thing
|
|
|
|
self.userdb.update_user(guild.id, targetuser.id, zoneinput)
|
|
|
|
await channel.send(':white_check_mark: Set zone for **' + targetuser.name + '#' + targetuser.discriminator + '**.')
|
|
|
|
|
2018-08-24 20:27:56 +00:00
|
|
|
async def cmd_list(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str):
|
|
|
|
wspl = msgcontent.split(' ', 1)
|
|
|
|
if len(wspl) == 1:
|
2020-03-01 21:45:41 +00:00
|
|
|
await self._list_noparam(guild, channel)
|
2018-08-24 20:27:56 +00:00
|
|
|
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.')
|
|
|
|
|
2019-12-27 03:51:32 +00:00
|
|
|
async def cmd_removeFor(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str):
|
|
|
|
if not self._isUserAdmin(author):
|
|
|
|
# Silently ignore
|
|
|
|
return
|
|
|
|
|
|
|
|
# Parameters: command, target
|
|
|
|
wspl = msgcontent.split(' ', 1)
|
|
|
|
|
|
|
|
if len(wspl) == 1:
|
|
|
|
await channel.send(":x: You must specify a user for whom to remove time zone data.")
|
|
|
|
return
|
|
|
|
targetuser = self._resolveUserParam(guild, wspl[1])
|
|
|
|
if targetuser is None:
|
|
|
|
await channel.send(":x: Unable to find the target user.")
|
|
|
|
return
|
|
|
|
|
|
|
|
self.userdb.delete_user(guild.id, targetuser.id)
|
|
|
|
await channel.send(':white_check_mark: Removed zone information for **' + targetuser.name + '#' + targetuser.discriminator + '**.')
|
|
|
|
|
2018-08-24 20:27:56 +00:00
|
|
|
# ------
|
|
|
|
# Supplemental command functions
|
|
|
|
|
|
|
|
async def _list_noparam(self, guild: discord.Guild, channel: discord.TextChannel):
|
2020-03-01 22:47:51 +00:00
|
|
|
userlist = self.userdb.get_users(guild.id)
|
|
|
|
if len(userlist) == 0:
|
2019-09-08 03:09:58 +00:00
|
|
|
await channel.send(':x: No users with registered time zones have been active in the last 30 days.')
|
2018-08-28 18:17:22 +00:00
|
|
|
return
|
2020-03-01 22:47:51 +00:00
|
|
|
|
|
|
|
orderedList = collections.OrderedDict(sorted(userlist.items()))
|
|
|
|
result = ''
|
|
|
|
|
|
|
|
for k, v in orderedList.items():
|
|
|
|
foundUsers = 0
|
|
|
|
line = k[4:] + ": "
|
|
|
|
for id in v:
|
|
|
|
member = await self._resolve_member(guild, id)
|
|
|
|
if member is None:
|
|
|
|
continue
|
|
|
|
if foundUsers > 10:
|
|
|
|
line += "and others... "
|
|
|
|
foundUsers += 1
|
|
|
|
line += self._userFormat(member) + ", "
|
|
|
|
if foundUsers > 0: result += line[:-2] + "\n"
|
|
|
|
|
|
|
|
em = discord.Embed(description=result.strip())
|
|
|
|
await channel.send(embed=em)
|
2018-08-28 18:17:22 +00:00
|
|
|
|
2018-08-24 20:27:56 +00:00
|
|
|
async def _list_userparam(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, param):
|
|
|
|
param = str(param)
|
2020-03-01 22:47:51 +00:00
|
|
|
usersearch = await self._resolve_member(guild, param)
|
2019-09-08 03:09:58 +00:00
|
|
|
if usersearch is None:
|
|
|
|
await channel.send(':x: Cannot find the specified user.')
|
2018-08-24 20:27:56 +00:00
|
|
|
return
|
2019-09-08 03:09:58 +00:00
|
|
|
|
2020-03-01 21:45:41 +00:00
|
|
|
res = self.userdb.get_user(guild.id, usersearch.id)
|
|
|
|
if res is None:
|
2019-09-08 03:09:58 +00:00
|
|
|
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.')
|
2018-08-24 20:27:56 +00:00
|
|
|
return
|
2020-03-01 22:47:51 +00:00
|
|
|
em = discord.Embed(description=tzPrint(res)[4:] + ": " + self._userFormat(usersearch))
|
2020-03-01 21:45:41 +00:00
|
|
|
await channel.send(embed=em)
|
2019-09-08 03:09:58 +00:00
|
|
|
|
2020-03-01 22:47:51 +00:00
|
|
|
async def _resolve_member(self, guild: discord.Guild, inputstr: str):
|
2019-09-08 03:09:58 +00:00
|
|
|
"""
|
2020-03-01 21:45:41 +00:00
|
|
|
Takes a string input and attempts to find the corresponding member.
|
2019-09-08 03:09:58 +00:00
|
|
|
"""
|
2020-03-03 00:44:31 +00:00
|
|
|
if guild.large: await self.dclient.request_offline_members(guild)
|
2019-09-08 03:09:58 +00:00
|
|
|
idsearch = None
|
|
|
|
try:
|
|
|
|
idsearch = int(inputstr)
|
|
|
|
except ValueError:
|
2020-03-01 22:47:51 +00:00
|
|
|
if inputstr.startswith('<@!') and inputstr.endswith('>'):
|
|
|
|
idsearch = int(inputstr[3:][:-1])
|
|
|
|
elif inputstr.startswith('<@') and inputstr.endswith('>'):
|
|
|
|
idsearch = int(inputstr[2:][:-1])
|
|
|
|
|
2019-09-08 03:09:58 +00:00
|
|
|
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
|