Restructured commands, moved to separate module

This commit is contained in:
Noikoio 2018-08-24 13:27:56 -07:00
parent dcbf21c82c
commit 0180dee93a
4 changed files with 187 additions and 158 deletions

176
client.py
View file

@ -4,172 +4,40 @@ import discord
import asyncio import asyncio
import aiohttp import aiohttp
from datetime import datetime from common import logPrint
import pytz
import settings import settings
from userdb import UserDatabase from userdb import UserDatabase
from commands import WtCommands
# For case-insensitive time zone lookup, map lowercase tzdata entries with
# entires with proper case. pytz is case sensitive.
tzlcmap = {x.lower():x for x in pytz.common_timezones}
timefmt = "%H:%M %d-%b %Z%z"
def tzPrint(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(timefmt), padding, zone)
def tsPrint(label, line):
"""
Print with timestamp in a way that resembles some of my other projects
"""
resultstr = datetime.utcnow().strftime('%Y-%m-%d %H:%m:%S') + ' [' + label + '] ' + line
print(resultstr)
class WorldTime(discord.Client): class WorldTime(discord.Client):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.udb = UserDatabase('users.db') self.userdb = UserDatabase('users.db')
self.commands = WtCommands(self.userdb, self)
self.bg_task = self.loop.create_task(self.periodic_report()) self.bg_task = self.loop.create_task(self.periodic_report())
async def on_ready(self): async def on_ready(self):
tsPrint('Status', 'Connected as {0} ({1})'.format(self.user.name, self.user.id)) logPrint('Status', 'Connected as {0} ({1})'.format(self.user.name, self.user.id))
# Command processing -----------------
async def cmd_help(self, message : discord.Message):
em = discord.Embed(
color=14742263,
title='Help & About',
description='This bot aims to answer the age-old question, "What time is it for everyone here?"')
em.set_footer(text='World Time', icon_url=self.user.avatar_url)
em.add_field(
name='Commands', value='''
`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='''
This bot uses zone names from the tz database. Most common zones are supported. For a list of entries, see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
''')
await message.channel.send(embed=em)
async def cmd_list(self, message):
wspl = message.content.split(' ', 1)
if len(wspl) == 1:
await self.cmd_list_noparam(message)
else:
await self.cmd_list_userparam(message, wspl[1])
async def cmd_list_noparam(self, message : discord.Message):
clist = self.udb.get_list(message.guild.id)
if len(clist) == 0:
await message.channel.send(':x: No users with known zones have been active in the last 72 hours.')
return
resultarr = []
for i in clist:
resultarr.append(tzPrint(i))
resultarr.sort()
resultstr = '```\n'
for i in resultarr:
resultstr += i + '\n'
resultstr += '```'
await message.channel.send(resultstr)
async def cmd_list_userparam(self, message, param):
# wishlist: search based on username/nickname
param = str(param)
if param.startswith('<@!') and param.endswith('>'):
param = param[3:][:-1]
if param.startswith('<@') and param.endswith('>'):
param = param[2:][:-1]
if not param.isnumeric():
# Didn't get an ID...
await message.channel.send(':x: You must specify a user by ID or `@` mention.')
return
res = self.udb.get_list(message.guild.id, param)
if len(res) == 0:
spaghetti = message.author.id == param
if spaghetti: await message.channel.send(':x: You do not have a time zone. Set it with `tz.set`.')
else: await message.channel.send(':x: The given user has not set a time zone. Ask to set it with `tz.set`.')
return
resultstr = '```\n' + tzPrint(res[0]) + '\n```'
await message.channel.send(resultstr)
async def cmd_time(self, message):
wspl = message.content.split(' ', 1)
if len(wspl) == 1:
# No parameter. So, doing the same thing anyway...
await self.cmd_list_userparam(message, message.author.id)
else:
try:
zoneinput = tzlcmap[wspl[1].lower()]
except KeyError:
await message.channel.send(':x: Not a valid zone name.')
return
resultstr = '```\n' + tzPrint(zoneinput) + '\n```'
await message.channel.send(resultstr)
async def cmd_set(self, message):
wspl = message.content.split(' ', 1)
if len(wspl) == 1:
# No parameter. But it's required
await message.channel.send(':x: Zone parameter is required.')
return
try:
zoneinput = tzlcmap[wspl[1].lower()]
except KeyError:
await message.channel.send(':x: Not a valid zone name.')
return
self.udb.update_user(message.guild.id, message.author.id, zoneinput)
await message.channel.send(':white_check_mark: Your zone has been set.')
async def cmd_remove(self, message):
self.udb.delete_user(message.guild.id, message.author.id)
await message.channel.send(':white_check_mark: Your zone has been removed.')
cmdlist = {
'help' : cmd_help,
'list' : cmd_list,
'time' : cmd_time,
'set' : cmd_set,
'remove': cmd_remove
}
async def command_dispatch(self, message):
'''Interprets incoming commands'''
cmdBase = message.content.split(' ', 1)[0].lower()
if cmdBase.startswith('tz.'):
cmdBase = cmdBase[3:]
else:
return
try:
await self.cmdlist[cmdBase](self, message)
tsPrint('Command invoked', '{0}/{1}: tz.{2}'.format(message.guild, message.author, cmdBase))
except KeyError:
pass
async def on_message(self, message): async def on_message(self, message):
# ignore bots (should therefore also ignore self) # ignore bots (should therefore also ignore self)
if message.author.bot: return if message.author.bot: return
# act on DMs
if isinstance(message.channel, discord.DMChannel): if isinstance(message.channel, discord.DMChannel):
tsPrint('Incoming DM', '{0}: {1}'.format(message.author, message.content.replace('\n', '\\n'))) await self.respond_dm(message)
await message.channel.send('''I don't work over DM. Only in servers.''')
# to do: small cache to not flood users who can't take a hint
return return
self.udb.update_activity(message.guild.id, message.author.id)
await self.command_dispatch(message) # Regular message
self.userdb.update_activity(message.guild.id, message.author.id)
cmdBase = message.content.split(' ', 1)[0].lower()
if cmdBase.startswith('tz.'): # wishlist: per-guild customizable prefix
cmdBase = cmdBase[3:]
await self.commands.dispatch(cmdBase, message)
async def respond_dm(self, message):
logPrint('Incoming DM', '{0}: {1}'.format(message.author, message.content.replace('\n', '\\n')))
await message.channel.send('''I don't work over DM. :frowning: Only in a server.''')
# to do: small cache to not flood users who can't take a hint
# ---------------- # ----------------
@ -186,7 +54,7 @@ This bot uses zone names from the tz database. Most common zones are supported.
await self.wait_until_ready() await self.wait_until_ready()
while not self.is_closed(): while not self.is_closed():
guildcount = len(self.guilds) guildcount = len(self.guilds)
tsPrint("Report", "Currently in {0} guild(s).".format(guildcount)) logPrint("Report", "Currently in {0} guild(s).".format(guildcount))
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
if authtoken != '': if authtoken != '':
rurl = "https://bots.discord.pw/api/bots/{}/stats".format(self.user.id) rurl = "https://bots.discord.pw/api/bots/{}/stats".format(self.user.id)
@ -194,7 +62,7 @@ This bot uses zone names from the tz database. Most common zones are supported.
rhead = { "Content-Type": "application/json", "Authorization": authtoken } rhead = { "Content-Type": "application/json", "Authorization": authtoken }
try: try:
await session.post(rurl, json=rdata, headers=rhead) await session.post(rurl, json=rdata, headers=rhead)
tsPrint("Report", "Reported count to Discord Bots.") logPrint("Report", "Reported count to Discord Bots.")
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
tsPrint("Report", "Discord Bots API report failed: {}".format(e)) logPrint("Report", "Discord Bots API report failed: {}".format(e))
await asyncio.sleep(21600) # Repeat once every six hours await asyncio.sleep(21600) # Repeat once every six hours

136
commands.py Normal file
View file

@ -0,0 +1,136 @@
# Command handlers
# Incoming messages that look like commands are passed into functions defined here.
from textwrap import dedent
import discord
from userdb import UserDatabase
from common import tzlcmap, tzPrint, logPrint
# All command functions are expected to have this signature:
# def cmd_NAME(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str)
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 functions
# ------
# Individual command handlers
async def cmd_help(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, msgcontent: str):
em = discord.Embed(
color=14742263,
title='Help & About',
description='This bot aims to answer the age-old question, "What time is it for everyone here?"')
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: 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' + 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_noparam(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):
clist = self.userdb.get_list(guild.id)
if len(clist) == 0:
await channel.send(':x: No users with known zones have been active in the last 72 hours.')
return
resultarr = []
for i in clist:
resultarr.append(tzPrint(i))
resultarr.sort()
resultstr = '```\n'
for i in resultarr:
resultstr += i + '\n'
resultstr += '```'
await channel.send(resultstr)
async def _list_userparam(self, guild: discord.Guild, channel: discord.TextChannel, author: discord.User, param):
# wishlist: search based on username/nickname
param = str(param)
if param.startswith('<@!') and param.endswith('>'):
param = param[3:][:-1]
if param.startswith('<@') and param.endswith('>'):
param = param[2:][:-1]
if not param.isnumeric():
# Didn't get an ID...
await channel.send(':x: You must specify a user by ID or `@` mention.')
return
res = self.userdb.get_list(guild.id, param)
if len(res) == 0:
spaghetti = author.id == param
if spaghetti: await channel.send(':x: You do not have a time zone. Set it with `tz.set`.')
else: await channel.send(':x: The given user has not set a time zone. Ask to set it with `tz.set`.')
return
resultstr = '```\n' + tzPrint(res[0]) + '\n```'
await channel.send(resultstr)

26
common.py Normal file
View file

@ -0,0 +1,26 @@
# Common items used throughout the project
import pytz
from datetime import datetime
# For case-insensitive time zone lookup, map lowercase tzdata entries with
# entires with proper case. pytz is case sensitive.
tzlcmap = {x.lower():x for x in pytz.common_timezones}
timefmt = "%H:%M %d-%b %Z%z"
def tzPrint(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(timefmt), padding, zone)
def logPrint(label, line):
"""
Print with timestamp in a way that resembles some of my other projects
"""
resultstr = datetime.utcnow().strftime('%Y-%m-%d %H:%m:%S') + ' [' + label + '] ' + line
print(resultstr)

View file

@ -4,10 +4,9 @@
# - https://github.com/Noikoio/WorldTime # - https://github.com/Noikoio/WorldTime
# - https://bots.discord.pw/bots/447266583459528715 # - https://bots.discord.pw/bots/447266583459528715
# Using discord.py rewrite. To install: # Dependencies (install via pip or other means):
# pip install -U git+https://github.com/Rapptz/discord.py@rewrite # pytz, sqlite3, discord.py@rewrite
# How to install the latter: pip install -U git+https://github.com/Rapptz/discord.py@rewrite
# And yes, this code sucks. I don't know Python all too well.
from discord import Game from discord import Game
from client import WorldTime from client import WorldTime