From 502a8d6b9e3d9a7adf9e38b01f4e773ad48bd320 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 12 Jun 2018 17:16:19 -0700 Subject: [PATCH] Switched to discord.py rewrite branch --- README.md | 2 +- WorldTime.py | 226 ----------------------------------- client.py | 169 ++++++++++++++++++++++++++ start.py | 28 +++++ UserDatabase.py => userdb.py | 5 + 5 files changed, 203 insertions(+), 227 deletions(-) delete mode 100644 WorldTime.py create mode 100644 client.py create mode 100644 start.py rename UserDatabase.py => userdb.py (94%) diff --git a/README.md b/README.md index e912c6e..fa893c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WorldTime -Invite link: https://discordapp.com/oauth2/authorize?client_id=447266583459528715&scope=bot +Info and invite link: https://bots.discord.pw/bots/447266583459528715 A common problem since the beginning of the Internet: You find yourself in an online community with users scattered all over different time zones. You seek people to talk to, but they're idle. You are reminded of the concept of time zones and begin to wonder: what if this is not a good time for them? diff --git a/WorldTime.py b/WorldTime.py deleted file mode 100644 index a4e2d7f..0000000 --- a/WorldTime.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 - -# World Time, a Discord bot. Displays user time zones. -# - https://github.com/Noikoio/WorldTime -# - https://bots.discord.pw/bots/447266583459528715 - -# ----------- -# Required: -bot_token = '' -# Optional. Don't delete or modify in a self-hosted instance. -dbots_token = '' -# ----------- - -# Bad code ahead. I knew next to nothing about Python and I also made hasty desicions throughout. -# Just keep that in mind if you decide to pick apart this code. - -# Not using discord.ext.commands extension, because see above. -# Namely, can't figure out how to use it while also having my own on_message. -# *And* I couldn't figure out how to customize its help message. - -import discord -import asyncio - -from datetime import datetime -import pytz - -from UserDatabase import UserDatabase - -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) - -# 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) - -db = UserDatabase('users.db') - -# --- -# Command things - -def build_help_embed(emtitle, emdesc): - em = discord.Embed( - color=14742263, - title=emtitle, - description=emdesc) - em.set_footer(text='World Time', icon_url=bot.user.avatar_url) - - return em - -async def cmd_help(message : discord.Message): - em = build_help_embed( - 'Help & About', - 'This bot aims to answer the age-old question, "What time is it for everyone here?"') - 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 bot.send_message(message.channel, embed=em) - -async def cmd_list(message : discord.Message): - wspl = message.content.split(' ', 1) - if len(wspl) == 1: - await cmd_list_noparam(message) - else: - await cmd_list_userparam(message, wspl[1]) -async def cmd_list_noparam(message : discord.Message): - clist = db.get_list(message.channel.server.id) - if len(clist) == 0: - await bot.send_message(message.channel, ':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 bot.send_message(message.channel, resultstr) -async def cmd_list_userparam(message : discord.Message, param): - # wishlist: search based on username/nickname - 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 bot.send_message(message.channel, ':x: You must specify a user by ID or `@` mention.') - return - res = db.get_list(message.channel.server.id, param) - if len(res) == 0: - spaghetti = message.author.id == param - if spaghetti: await bot.send_message(message.channel, ':x: You do not have a time zone. Set it with `tz.set`.') - else: await bot.send_message(message.channel, ':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 bot.send_message(message.channel, resultstr) - -async def cmd_time(message : discord.Message): - wspl = message.content.split(' ', 1) - if len(wspl) == 1: - # No parameter. So, doing the same thing anyway... - await cmd_list_userparam(message, message.author.id) - else: - try: - zoneinput = tzlcmap[wspl[1].lower()] - except KeyError: - await bot.send_message(message.channel, ':x: Not a valid zone name.') - return - resultstr = '```\n' + tzPrint(zoneinput) + '\n```' - await bot.send_message(message.channel, resultstr) - -async def cmd_set(message : discord.Message): - wspl = message.content.split(' ', 1) - if len(wspl) == 1: - # No parameter. But it's required - await bot.send_message(message.channel, ':x: Zone parameter is required.') - return - try: - zoneinput = tzlcmap[wspl[1].lower()] - except KeyError: - await bot.send_message(message.channel, ':x: Not a valid zone name.') - return - db.update_user(message.channel.server.id, message.author.id, zoneinput) - await bot.send_message(message.channel, ':white_check_mark: Your zone has been set.') - -async def cmd_remove(message : discord.Message): - db.delete_user(message.channel.server.id, message.author.id) - await bot.send_message(message.channel, ':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(message : discord.Message): - '''Interprets incoming commands''' - thecmd = message.content.split(' ', 1)[0].lower() - if thecmd.startswith('tz.'): - thecmd = thecmd[3:] - else: - return - try: - await cmdlist[thecmd](message) - tsPrint('Command invoked', '{0}/{1}: tz.{2}'.format(message.channel.server, message.author, thecmd)) - except KeyError: - pass - -# --- -# Bot things - -bot = discord.Client() - -@bot.event -async def on_ready(): - tsPrint('Status', 'Connected as {0} ({1})'.format(bot.user.name, bot.user.id)) - await bot.change_presence(game=discord.Game(name='tz.help')) - -@bot.event -async def on_message(message : discord.Message): - # ignore bots (should therefore also ignore self) - if message.author.bot: return - # act on DMs - if isinstance(message.channel, discord.PrivateChannel): - tsPrint('Incoming DM', '{0}: {1}'.format(message.author, message.content.replace('\n', '\\n'))) - await bot.send_message(message.channel, '''I can't work over DMs...''') - # to do: small cache to not flood users who can't take a hint - return - db.update_activity(message.server.id, message.author.id) - await command_dispatch(message) - -# --- -# This periodically sends the server count to Discord Bots. -# If you're not the original author of this bot, you have no reason at all to be using this. -if dbots_token != '': - import aiohttp - async def dbots_update(): - await bot.wait_until_ready() - while not bot.is_closed: - serverct = len(bot.servers) - async with aiohttp.ClientSession() as session: - rurl = "https://bots.discord.pw/api/bots/{}/stats".format(bot.user.id) - rdata = { "server_count": serverct } - rhead = { "Content-Type": "application/json", "Authorization": dbots_token } - try: - await session.post(rurl, json=rdata, headers=rhead) - tsPrint("Report", "Reporting {0} guild(s) to Discord Bots.".format(serverct)) - except aiohttp.ClientError as e: - tsPrint("Report", "Discord Bots API report failed: {}".format(e)) - await asyncio.sleep(21600) # Update once every six hours - -# --- - -if __name__ == '__main__': - tsPrint('Status', 'Hello. Using users.db for database.') - if dbots_token != '': - bot.loop.create_task(dbots_update()) - bot.run(bot_token) \ No newline at end of file diff --git a/client.py b/client.py new file mode 100644 index 0000000..be95669 --- /dev/null +++ b/client.py @@ -0,0 +1,169 @@ +# WorldTime Discord client + +import discord +import asyncio + +from datetime import datetime +import pytz + +import aiohttp +from userdb import UserDatabase + +# 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): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.udb = UserDatabase('users.db') + + async def on_ready(self): + tsPrint('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 + 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): + # ignore bots (should therefore also ignore self) + if message.author.bot: return + # act on DMs + if isinstance(message.channel, discord.DMChannel): + tsPrint('Incoming DM', '{0}: {1}'.format(message.author, message.content.replace('\n', '\\n'))) + 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 + self.udb.update_activity(message.guild.id, message.author.id) + await self.command_dispatch(message) \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 0000000..c668a58 --- /dev/null +++ b/start.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +# World Time, a Discord bot. Displays user time zones. +# - https://github.com/Noikoio/WorldTime +# - https://bots.discord.pw/bots/447266583459528715 + +# Using discord.py rewrite. To install: +# pip install -U git+https://github.com/Rapptz/discord.py@rewrite + +# And yes, this code sucks. I don't know Python all too well. + +# -------------------------------- +# Required: +bot_token = '' +# -------------------------------- + +from discord import Game +from client import WorldTime + +if __name__ == '__main__': + try: + if bot_token == '': raise NameError() # <- dumb + except NameError: + print("Bot token not set. Backing out.") + exit() + + client = WorldTime(activity=Game('tz.help')) + client.run(bot_token) diff --git a/UserDatabase.py b/userdb.py similarity index 94% rename from UserDatabase.py rename to userdb.py index 8d3308f..3d02490 100644 --- a/UserDatabase.py +++ b/userdb.py @@ -1,7 +1,12 @@ +# Abstracts away details of the SQLite database that stores user information. + import sqlite3 class UserDatabase: def __init__(self, dbname): + ''' + Sets up the SQLite session for the user database. + ''' self.db = sqlite3.connect(dbname) cur = self.db.cursor() cur.execute('''CREATE TABLE IF NOT EXISTS users(