mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-24 01:14:13 +00:00
Restructured commands, moved to separate module
This commit is contained in:
parent
dcbf21c82c
commit
0180dee93a
4 changed files with 187 additions and 158 deletions
176
client.py
176
client.py
|
@ -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
136
commands.py
Normal 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
26
common.py
Normal 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)
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue