From: CMDR furrycat Date: Wed, 11 Nov 2020 08:26:30 +0000 (+0100) Subject: Use discord.py 1.5.1. X-Git-Url: http://git.furryclan.net/?a=commitdiff_plain;h=6f36f0e7c9d839fbb2cb17d4696d16908ed68902;p=furrycat%2Fcatbot.git Use discord.py 1.5.1. Application code and all plugins migrated to the new API. Startup now respects CATBOT_SHARD_ID and CATBOT_SHARD_COUNT environment variables. Typing context managers can be shared between message send invocations. --- diff --git a/app.py b/app.py index eee2a72..ebee67f 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ import asyncio import datetime -import discord_unofficial as discord +import discord import logging import multiprocessing import os @@ -39,19 +39,36 @@ for logger in ['catbot', 'eddb', 'eddn']: plugins = Plugins(log.level) idletime = multiprocessing.Value('i', 900) +discord_ready = multiprocessing.Value('b', False) threads_ready = multiprocessing.Value('b', False) -async def say_expect(message, stdout): +value = os.getenv('CATBOT_SHARD_ID') +if value: + try: + shard_id = int(value) + except: + log.error('Invalid shard ID: {}'.format(value)) + sys.exit(1) + +value = os.getenv('CATBOT_SHARD_COUNT') +if value: + try: + shard_count = int(value) + except: + log.error('Invalid shard count: {}'.format(value)) + sys.exit(1) + +async def say_expect(message, stdout, typing = None): if stdout: log.debug(stdout) - result = await bot.say(message.channel, '{}```{}```'.format('{} '.format(message.author.mention) if not message.channel.is_private else '', stdout)) + result = await bot.say(message.channel, '{}```{}```'.format('{} '.format(message.author.mention) if not bot.is_private(message.channel) else '', stdout), typing = typing) else: - result = await bot.say(message.channel, 'Sorry!') + result = await bot.say(message.channel, 'Sorry!', typing = typing) return result -async def not_admin(message): +async def not_admin(message, **args): log.warning('Not admin: {} id {}'.format(message.author, message.author.id)) - await bot.maybe_say(message.channel, 'hiss!') + await bot.maybe_say(message.channel, 'hiss!', **args) async def show_help(message, *args): if len(args): @@ -63,7 +80,7 @@ async def show_help(message, *args): valid_commands = ['id', 'stop playing', 'time', 'url'] valid_commands += plugins.valid_commands() lines = [ - '<@{}>, the cat-like robot from the 34th century of the future, recognises these commands (and more):'.format(client.user.id), + 'Version 2 of <@{}>, the cat-like robot from the 34th century of the future, recognises these commands (and more):'.format(client.user.id), '```' ] lines += sorted(valid_commands) @@ -93,7 +110,7 @@ async def show_help(message, *args): async def do_commands(message, command, raw, non_admin): if not non_admin: - if not bot.is_admin(message.author, message.server): + if not bot.is_admin(message.author, message.guild): await not_admin(message) return @@ -118,7 +135,7 @@ async def do_commands(message, command, raw, non_admin): elif command == 'delete': # delete - if message.channel.is_private: + if bot.is_private(message.channel): await cat.hiss(message.channel, wake = False) return @@ -128,7 +145,7 @@ async def do_commands(message, command, raw, non_admin): if m is not None: if m.group(1) != 'all': limit = int(m.group(1)) - await client.purge_from(message.channel, *args, limit = limit) + await message.channel.purge(*args, limit = limit) return elif command == 'help': @@ -162,7 +179,7 @@ async def do_commands(message, command, raw, non_admin): elif command == 'time': # time await bot.say(message.channel, bot.iso8601(int(time.time()))) - await bot.maybe_play_sound(bot.voice_channel_for_user(message.author), 'purr.wav', join = False) + await cat.maybe_purr(message.channel, voice = bot.voice_channel_for_user(message.author), join = False, wake = False) return elif command == 'url': @@ -170,17 +187,16 @@ async def do_commands(message, command, raw, non_admin): await bot.say(message.channel, "<{}>".format(discord.utils.oauth_url(client.user.id))) return - await bot.maybe_say(message.channel, 'purr', wake = False) - await bot.maybe_play_sound(bot.voice_channel_for_user(message.author), 'purr.wav', join = False) + await cat.maybe_purr(message.channel, voice = bot.voice_channel_for_user(message.author), join = False, wake = False) -async def do_fork(message, command, raw): - await client.send_typing(message.channel) +async def do_fork(message, command, raw, typing = None): if re.search(r'[^A-Za-z0-9-_."\'\s]', raw): await cat.hiss(message.channel) - return - p = pexpect.spawnu('./{}'.format(raw)) - p.expect(pexpect.EOF) - await say_expect(message, p.before) + else: + with bot.typing_context_manager(message.channel, typing) as typing: + p = pexpect.spawnu('./{}'.format(raw)) + p.expect(pexpect.EOF) + await say_expect(message, p.before, typing = typing) async def maybe_sleep(): while True: @@ -191,10 +207,10 @@ async def maybe_sleep(): await asyncio.sleep(60) async def request_offline_members(): - for server in client.servers: + for server in client.guilds: if server.large: log.info('Requesting offline user list from {}'.format(server.name)) - await client.request_offline_members(server) + await server.chunk() modules = { 'admin': { @@ -216,7 +232,11 @@ modules = { loop = eddn.init() asyncio.set_event_loop(loop) -client = discord.Client() +client = discord.Client( + intents = discord.Intents.all(), + shard_id = shard_id, shard_count = shard_count, + assume_unsync_clock = False + ) bot.client = client bot.db = db plugins.set_client(client) @@ -270,6 +290,8 @@ async def process_module(message, command, raw): @client.event async def on_ready(): + discord_ready.value = True + log.info('{} can now receive commands!'.format(client.user.name)) state = db.get_state(client) if state is not None: if state['idle']: @@ -277,10 +299,10 @@ async def on_ready(): await bot.set_idle(True) if state['avatar'] != db.IDLE_AVATAR: log.info('Restoring idle avatar.') - await set_avatar(db.IDLE_AVATAR) + await bot.set_avatar(db.IDLE_AVATAR) elif state['avatar'] != db.ONLINE_AVATAR: log.info('Restoring online avatar.') - await set_avatar(db.ONLINE_AVATAR) + await bot.set_avatar(db.ONLINE_AVATAR) if state['last_spoke']: bot.set('last_spoke', state['last_spoke']) log.info('Logged in as {}#{}'.format(client.user.name, client.user.id)) @@ -292,7 +314,7 @@ async def on_ready(): threads_ready.value = True def mentioned_in(message, explicit = True): - if message.channel.is_private: + if bot.is_private(message.channel): return True if not client.user.mentioned_in(message): m = re.match('<@!{}>'.format(client.user.id), message.content) @@ -312,7 +334,7 @@ async def on_typing(channel, user, when): return if user.bot: return - if channel.is_private: + if bot.is_private(channel): return log.debug('{} is typing in {}'.format(user, channel)) await plugins.on_typing(channel, user, when) @@ -331,12 +353,12 @@ async def on_message(message): return # Strip our mention from the message. - raw = re.sub('<@!?{}>'.format(client.user.id), '', message.content).strip() + raw = re.sub(r'<@!?{}>'.format(client.user.id), '', message.content).strip() # Handle copy/paste; Discord client will prepend (BOT): to pasted # text. Deal with the common cases of the user copying his own message # or quoting the bot. - raw = re.sub('^(?:BOT{}|{}):\s+'.format(client.user.name, message.author.name), '', raw) + raw = re.sub(r'^(?:BOT{}|{}):\s+'.format(client.user.name, message.author.name), '', raw) m = re.match(r'^(\S+)', raw) if m is None: @@ -357,8 +379,19 @@ async def on_member_join(member): async def on_member_update(before, after): await plugins.on_member_update(before, after) +async def await_login(): + backoff = 1 + while not discord_ready.value: + if backoff > 1: + log.info('Waiting for Discord login...') + await asyncio.sleep(backoff) + backoff *= 2 + if backoff > 4: + backoff = 4 + ret = 0 try: + asyncio.ensure_future(await_login()) log.info('Logging in to Discord.') loop.run_until_complete(client.login(token)) log.info('Opening WebSocket.') diff --git a/bot.py b/bot.py index 3333d45..f66d89d 100644 --- a/bot.py +++ b/bot.py @@ -1,6 +1,6 @@ import asyncio import datetime -import discord_unofficial as discord +import discord import hashlib import io import math @@ -16,6 +16,8 @@ import time import traceback import urllib.request +import cat + variables = { 'dryrun': multiprocessing.Value('b', os.getenv('DRYRUN') is not None), 'debug': multiprocessing.Value('b', os.getenv('DEBUG') is not None), @@ -25,6 +27,10 @@ variables = { 'interrupt_playing': multiprocessing.Value('b', False) } +def format_frame(frame): + dirname, basename = frame.filename.split(os.path.sep)[-2:] + return '{}@{}:{}'.format(frame.name, dirname if basename == '__init__.py' else basename, frame.lineno) + def is_admin(user, server = None, context = None): ret = False params = ['global'] @@ -42,12 +48,12 @@ def is_admin(user, server = None, context = None): break db.close_db() tb = reversed(traceback.extract_stack()[0:-2]) - log.info('{}: User {} {} an administrator{}'.format(': '.join(['{}@{}:{}'.format(frame[2], frame[0].split('/')[-1], frame[1]) for frame in tb]), user.name, 'is' if ret else 'is not', ' on server {}'.format(server.name) if server is not None else '')) + log.info('{}: User {} {} an administrator{}'.format(': '.join([format_frame(frame) for frame in tb]), user.name, 'is' if ret else 'is not', ' on server {}'.format(server.name) if server is not None else '')) return ret def is_member(member, name, *, author = None, check_nick = True): if author is not None: - if not member.server.get_member(author.id): + if not member.guild.get_member(author.id): return False parts = name.split('#') member_name = parts[0].lower() @@ -66,6 +72,24 @@ def is_member(member, name, *, author = None, check_nick = True): return False return True +def is_private(channel): + return isinstance(channel, discord.abc.PrivateChannel) + +def is_everyone(role): + return role.id == role.guild.roles[0].id + +def canonicalise(user): + return '{}#{}'.format(user.name, user.discriminator) + +def snowflake(id): + if id is None: + return None + try: + return int(id) + except ValueError: + log.error('Invalid snowflake: {}'.format(id)) + return None + def get_member_by_name(name, author = None): candidates = [] for member in client.get_all_members(): @@ -145,6 +169,24 @@ def unparse_seconds(seconds): interval += '{}s'.format(seconds) return interval +def get_server(id): + return client.get_guild(snowflake(id)) + +def get_channel(id): + return client.get_channel(snowflake(id)) + +def get_message_servers(message, allow_private = False): + servers = [] + if is_private(message.channel): + if allow_private: + servers.append('private') + for server in client.guilds: + if message.author in server.members: + servers.append(server) + else: + servers = [message.channel.guild] + return servers + def shared_with(user, instance, allow_admin_override = True): if user is None: return True @@ -153,17 +195,17 @@ def shared_with(user, instance, allow_admin_override = True): if allow_admin_override and is_admin(user): return True - if type(instance) == discord.server.Server: + if type(instance) == discord.guild.Guild: servers = [instance] elif type(instance) in [discord.member.Member, discord.user.User]: servers = [] - for server in client.servers: + for server in client.guilds: if server.get_member(instance.id): servers.append(server) - elif type(instance) == discord.channel.Channel: - servers = [instance.server] + elif isinstance(instance, discord.abc.GuildChannel): + servers = [instance.guild] elif type(instance) == discord.role.Role: - servers = [instance.server] + servers = [instance.guild] else: log.warning("Don't know how to check if a {} is known to {}!".format(type(instance), user)) return True @@ -193,7 +235,7 @@ def parse_server(text, *, author = None): if author is not None: log.warning("User {} isn't on parsed server {}!".format(author, server.name)) - for server in client.servers: + for server in client.guilds: if server.name.lower() == text.lower(): if shared_with(author, server): return server @@ -204,6 +246,7 @@ def parse_server(text, *, author = None): return None def parse_channel(text, *, author = None, text_ok = True, voice_ok = True): + text = str(text) channel_type = '{}, {}'.format('text' if text_ok else 'non-text', 'voice' if voice_ok else 'non-voice') log.debug("Parsing {} channel {}".format(channel_type, text)) if author is None: @@ -223,9 +266,9 @@ def parse_channel(text, *, author = None, text_ok = True, voice_ok = True): if m is None: m = re.match(r'#?(\d+)', text) if m is not None: - channel_id = m.group(1) + channel_id = snowflake(m.group(1)) log.debug('Channel ID {}.'.format(channel_id)) - channel = client.get_channel(channel_id) + channel = get_channel(channel_id) if not channel: log.warning("Can't find channel with ID {}!".format(channel_id)) return None @@ -238,7 +281,7 @@ def parse_channel(text, *, author = None, text_ok = True, voice_ok = True): if shared_with(author, channel): return channel if author is not None: - log.warning("User {} isn't on channel {} server {}!".format(author, channel.name, server.name)) + log.warning("User {} isn't on channel {} server {}!".format(author, channel.name, channel.guild.name)) # #name or name. if text[0] == '#': @@ -248,7 +291,7 @@ def parse_channel(text, *, author = None, text_ok = True, voice_ok = True): for channel in client.get_all_channels(): # Note that text channels are all lowercase; voice channels are title case. if channel.name.lower() == name.lower(): - log.debug('Channel {} has ID {} on server {}.'.format(channel.name, channel.id, channel.server.id)) + log.debug('Channel {} has ID {} on server {}.'.format(channel.name, channel.id, channel.guild.id)) if not text_ok and channel.type != discord.ChannelType.voice: log.warning('Parsed channel {} of wrong type!'.format(channel_id)) continue @@ -258,7 +301,7 @@ def parse_channel(text, *, author = None, text_ok = True, voice_ok = True): if shared_with(author, channel): return channel if author is not None: - log.warning("User {} isn't on channel {} server {}!".format(author, channel.name, server.name)) + log.warning("User {} isn't on channel {} server {}!".format(author, channel.name, channel.guild.name)) log.warning("Can't find {} channel {}!".format(channel_type, text)) return None @@ -285,9 +328,9 @@ def parse_mention(text, *, author = None): if m is None: m = re.match(r'@?&(\d+)', text) if m is not None: - role_id = m.group(1) + role_id = snowflake(m.group(1)) log.debug('Role ID {}.'.format(role_id)) - for server in client.servers: + for server in client.guilds: member = server.get_member(author.id) if member is None: log.debug('Role {} is not known to {} on {}.'.format(role_id, author, server.name)) @@ -307,7 +350,7 @@ def parse_mention(text, *, author = None): if m is None: m = re.match(r'@?(\d+)', text) if m is not None: - member_id = m.group(1) + member_id = snowflake(m.group(1)) log.debug('Member {}.'.format(member_id)) log.info('Member {}'.format(member_id)) return member_id @@ -317,7 +360,7 @@ def parse_mention(text, *, author = None): if m is not None: name = m.group(2).lower() log.debug('Role {}.'.format(name)) - for server in client.servers: + for server in client.guilds: member = server.get_member(author.id) if member is None: log.debug('Role {} is not known to {} on {}.'.format(name, author, server.name)) @@ -333,7 +376,7 @@ def parse_mention(text, *, author = None): log.debug('Member {}.'.format(name)) member = get_member_by_name(name, author) if member is not None: - log.info('Parsed {} as {}#{} on {}'.format(name, member.name, member.discriminator, member.server.name)) + log.info('Parsed {} as {} on {}'.format(name, canonicalise(member), member.guild.name)) return member.id log.warning("Can't find role or member with name {}!".format(name)) return None @@ -348,14 +391,14 @@ def parse_mention(text, *, author = None): for channel in client.get_all_channels(): if channel.name.lower() != name: continue - member = channel.server.get_member(author.id) + member = channel.guild.get_member(author.id) if member is None: - log.warning('Channel {} is not known to {} on {}.'.format(name, author, channel.server.name)) + log.warning('Channel {} is not known to {} on {}.'.format(name, author, channel.guild.name)) continue if channel.type == discord.ChannelType.voice: log.warning('Channel {} is a voice channel!'.format(channel.name)) continue - log.info('Parsed channel {} as {} on {}.'.format(name, channel.name, channel.server.name)) + log.info('Parsed channel {} as {} on {}.'.format(name, channel.name, channel.guild.name)) return '#{}'.format(channel.id) log.warning("Can't find channel with name {}!".format(name)) return None @@ -369,9 +412,10 @@ async def parse_user(text, *, author = None): parsed = parse_mention(text, author = author) if parsed is None: return None - if not re.match(r'^\d+$', parsed): + user_id = str(parsed) + if not re.match(r'^\d+$', user_id): return None - user = await client.get_user_info(parsed) + user = await client.fetch_user(user_id) return user async def parse_role(text, *, author = None, server = None): @@ -383,13 +427,13 @@ async def parse_role(text, *, author = None, server = None): if parsed in ['everyone', 'here']: if author is not None: if isinstance(author, discord.member.Member): - return author.server.default_role + return author.guild.default_role m = re.match(r'^&(\d+)$', parsed) if not m: return None - id = m.group(1) + id = snowflake(m.group(1)) if server is None: - servers = client.servers + servers = client.guilds else: servers = [server] for server in servers: @@ -420,7 +464,7 @@ def open_url(url): # Get my idle status. def status(): - for server in client.servers: + for server in client.guilds: return server.me.status return None @@ -433,7 +477,7 @@ async def set_avatar(url): log.debug('Failed to open URL {} for avatar'.format(url)) return False try: - await client.edit_profile(avatar = fd.read()) + await client.user.edit(avatar = fd.read()) fd.close() return True except: @@ -441,18 +485,27 @@ async def set_avatar(url): log.exception("set_avatar") return False -async def set_idle(idle): - await client.change_presence(status = discord.Status.idle if idle else discord.Status.online) - for voice in list(client.voice_clients): - await voice.disconnect() +async def set_idle(idle, activity = None): + await client.change_presence( + activity = activity if activity else discord.Activity(name = 'sleeping', start = datetime.datetime.utcnow(), details = 'zzz') if idle else None, + status = discord.Status.idle if idle else discord.Status.online + ) + if idle: + for voice in list(client.voice_clients): + await voice.disconnect() + +async def set_activity(activity = None): + await client.change_presence(activity = activity) -async def wake_up(force = False): +async def wake_up(force = False, activity = None): set('last_spoke', time.time()) if force or status() != discord.Status.online: log.info('Waking up...') await set_avatar(db.ONLINE_AVATAR) - await set_idle(False) + await set_idle(False, activity = activity) db.set_state(client, avatar = db.ONLINE_AVATAR, idle = False, last_spoke = get('last_spoke')) + elif activity: + await set_idle(False, activity = activity) async def zzz(): # state = db.get_state(client) @@ -506,22 +559,51 @@ def parse_attachment(attachment, filename = None): return parsed -async def maybe_say(channel, text, *, attachment = None, embed = None, probability = 0.2, wake = True): +def typing_context_manager(channel, typing = None): + class DummyContextManager(object): + def __init__(self): + return + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return + + return DummyContextManager() if typing else channel.typing() + +# join may be passed by cat.maybe_purr() etc. +async def maybe_say(channel, text, *, typing = None, attachment = None, embed = None, probability = 0.2, wake = True, join = None): if random.random() <= probability: + message = None try: - message = None - await client.send_typing(channel) - if attachment is not None: - parsed = parse_attachment(attachment) - if parsed is not None: - message = await client.send_file(channel, parsed['fd'], filename = parsed['filename'], content = text) - parsed['fd'].close() - else: - log.warning("Missing or invalid attachment!") - if message is None: - message = await client.send_message(channel, text, embed = embed) + ok = True + send = {} + async with typing_context_manager(channel, typing) as context_manager: + if embed: + send['embed'] = embed + if attachment is not None: + parsed = parse_attachment(attachment) + if parsed is not None: + send['file'] = discord.File(parsed['fd'], parsed['filename']) + parsed['fd'].close() + else: + log.warning("Missing or invalid attachment!") + ok = False + if ok: + message = await channel.send(text, **send) except Exception: - destination = 'private channel [{}]'.format(', '.join(user.name, user.discriminator) for user in channel.recipients if user.id != channel.me.id) if channel.is_private else 'channel {} on {}'.format(channel.name, channel.server.name) + if is_private(channel): + recipients = [user for user in channel.recipients if user.id != channel.me.id] if isinstance(channel, discord.GroupChannel) else [channel.recipient] + destination = 'private channel [{}]'.format(', '.join([canonicalise(user) for user in recipients])) + else: + destination = 'channel {} on {}'.format(channel.name, channel.guild.name) log.error('sending to {}: {}'.format(destination, traceback.format_exc().splitlines()[-1])) if wake: await wake_up() @@ -530,8 +612,10 @@ async def maybe_say(channel, text, *, attachment = None, embed = None, probabili log.debug("Didn't bother to say {}".format(text)) return None -async def say(channel, text, *, attachment = None, embed = None, wake = None): +async def say(channel, text, *, typing = None, attachment = None, embed = None, wake = None, join = None): args = { 'probability': 1.0 } + if typing is not None: + args['typing'] = typing if attachment is not None: args['attachment'] = attachment if embed is not None: @@ -541,54 +625,137 @@ async def say(channel, text, *, attachment = None, embed = None, wake = None): message = await maybe_say(channel, text, **args) return message -async def say_many(channel, lines, *, attachment = None, embed = None, maxlen = 2000): - while len(lines): - message_len = 0 - n = 0 - for i in range(0, len(lines)): - message_len += len(lines[i]) - if i: - # Include newlines. - message_len += 1 - if message_len > maxlen: +async def say_many(channel, lines, *, typing = None, attachment = None, embed = None, maxlen = 2000): + async with typing_context_manager(channel, typing) as context_manager: + while len(lines): + message_len = 0 + n = 0 + for i in range(0, len(lines)): + message_len += len(lines[i]) + if i: + # Include newlines. + message_len += 1 + if message_len > maxlen: + break + n += 1 + # We can't print too big a message. + subset = lines[:n] + message = '\n'.join(subset) + result = await say(channel, message, typing = context_manager, attachment = attachment, embed = embed) + if not result: break - n += 1 - # We can't print too big a message. - subset = lines[:n] - message = '\n'.join(subset) - result = await say(channel, message, attachment = attachment, embed = embed) - if not result: - break - lines = lines[len(subset):] - attachment = None - embed = None + lines = lines[len(subset):] + attachment = None + embed = None -def voice_channel_for_channel(channel): +def voice_client_for_server(server): for voice in client.voice_clients: - if voice.server != channel.server: - continue - if not voice.is_connected: + if voice.guild != server: continue + return voice + +def voice_client_for_channel(channel): + return voice_client_for_server(channel.guild) + +def voice_client_for_user(user): + voice_channel = voice_channel_for_user(user) + if voice_channel is None: + return None + return voice_client_for_server(voice_channel.guild) + +def voice_channel_for_server(server): + voice = voice_client_for_server(server) + if voice: return voice.channel def voice_channel_for_user(user): - for server in client.servers: + for server in client.guilds: member = server.get_member(user.id) if not member: continue - voice_channel = member.voice_channel + if not member.voice: + continue + voice_channel = member.voice.channel if voice_channel is not None: - if voice_channel.server in client.servers: + if voice_channel.guild in client.guilds: return voice_channel -async def create_player(voice, url): +async def maybe_join_voice_channel(channel, join = False): + voice = voice_client_for_server(channel.guild) + if voice is None: + if not join: + return None + log.debug('Joining voice channel {} on {}'.format(channel.name, channel.guild.name)) + voice = await channel.connect() + if not voice: + log.error("Can't join voice channel {} on {}!".format(channel.name, channel.guild.name)) + voice.stop() + return voice + if voice.channel == channel: + if not voice.is_connected(): + await voice.connect() + return voice + if voice.is_playing(): + if not join: + log.debug('Not interrupting current sound to move to {} on {}'.format(channel.name, channel.guid.name)) + return None + log.debug('Moving to voice channel {} on {}'.format(channel.name, channel.guild.name)) + await voice.move_to(channel) + await asyncio.sleep(1) + if voice.channel != channel: + log.error("Can't move to voice channel {} on {}!".format(channel.name, channel.guild.name)) + return None + voice.stop() + return voice + +async def join_voice_channel(channel): + voice = await maybe_join_voice_channel(channel, True) + return voice + +def tempfile_for_voice_channel(channel): + return os.path.join(tempfile.gettempdir(), str(channel.id)) + +async def create_youtube_audio(voice, url, name = None): + import youtube_dl + filename = tempfile_for_voice_channel(voice.channel) + options = { + 'format': 'bestaudio/best', + 'outtmpl': filename, + 'nooverwrites': False, + 'noplaylist': True, + 'nocheckcertificate': True, + 'ignoreerrors': False, + 'logtostderr': False, + 'quiet': True, + 'no_warnings': True, + 'default_search': 'auto' + } + + def after(error): + try: + os.unlink(filename) + except Exception as e: + log.error('Deleting Youtube temporary file {}: {}'.format(filename, str(e))) + + try: + ytdl = youtube_dl.YoutubeDL(options) + info = ytdl.extract_info(url) + return [discord.FFmpegPCMAudio(filename), discord.Streaming(name = name if name else info.get('title', 'YouTube'), platform = 'YouTube', url = url), after] + except: + log.exception('Downloading {}'.format(url)) + return [None, None, None] + +async def create_audio(voice, url, name = None): if re.match(r'https?://(?:www\.)?youtu(?:\.be|be\.com)/', url): - player = await voice.create_ytdl_player(url) + audio, activity, after = await create_youtube_audio(voice, url, name = name) else: - player = voice.create_ffmpeg_player(url) - return player + audio = discord.FFmpegPCMAudio(url) + activity = discord.Game(name = name if name else url) + after = None + return [audio, activity, after] -async def maybe_play_sound(channel, filename, *, probability = 0.05, join = True): +# wake may be passed by cat.maybe_purr() etc. +async def maybe_play_sound(channel, filename, *, name = None, probability = 0.05, join = True, wake = None): if not channel or not filename: if join: log.warning('Missing channel and/or filename for play_sound()') @@ -602,47 +769,47 @@ async def maybe_play_sound(channel, filename, *, probability = 0.05, join = True await asyncio.sleep(1) set('playing', True) - voice = client.voice_client_in(channel.server) - if voice is None or voice.channel != channel: + voice = await maybe_join_voice_channel(channel, join) + if not voice: if not join: log.info('Not joining voice channel {} just to play {}'.format(channel, filename)) - return False - log.debug('Moving to voice channel {}'.format(channel)) - if voice is not None: - await voice.move_to(channel) - await asyncio.sleep(1) - else: - await client.join_voice_channel(channel) - voice = client.voice_client_in(channel.server) - if voice.channel != channel: - log.error("Can't move to voice channel {}".format(channel)) - set('playing', False) - return False + set('playing', False) + return False try: log.info('Playing {} in channel {}'.format(filename, channel)) - player = await create_player(voice, filename) - player.start() - while not player.is_done(): + audio, activity, after = await create_audio(voice, filename, name = name) + if audio is None: + log.error("Can't initialise audio stream from {}!".format(filename)) + set('playing', False) + await set_activity(None) + return False + await wake_up(activity = activity) + voice.play(audio, after = after) + while voice.is_playing(): if get('interrupt_playing'): log.info('Interrupted!') - player.stop() + voice.stop() break await asyncio.sleep(1) set('playing', False) set('interrupt_playing', False) + await set_activity(None) log.info('Finished playing {} in channel {}'.format(filename, channel)) return True except: - log.exception("Error creating ffpmeg player for {}".format(filename)) + log.exception("Creating audio source for {}".format(filename)) set('playing', False) set('interrupt_playing', False) + await set_activity(None) return False -async def play_sound(channel, filename, *, join = True): - result = await maybe_play_sound(channel, filename, probability = 1.0, join = join) +async def play_sound(channel, filename, *, name = None, join = True, wake = None): + result = await maybe_play_sound(channel, filename, name = name, probability = 1.0, join = join) return result -async def tts(channel, text, args = {}): +async def tts(channel, text, args = {}, name = None): + if not name: + name = 'text-to-speech' try: fd, wav = tempfile.mkstemp('.tts') os.close(fd) @@ -668,18 +835,18 @@ async def tts(channel, text, args = {}): os.unlink(wav) return False - await play_sound(channel, wav) + await play_sound(channel, wav, name = name) os.unlink(wav) return True def lower_role_than(member, *, strict = False): - if member.server.me is None: + if member.guild.me is None: log.error("Can't call lower_role_than() a member on a server we aren't on!") return False if strict: - return member.top_role > member.server.me.top_role + return member.top_role > member.guild.me.top_role else: - return member.top_role >= member.server.me.top_role + return member.top_role >= member.guild.me.top_role def lower_role_on_server_than(server, id, *, strict = False): member = server.get_member(id) @@ -698,7 +865,7 @@ def equal_permissions(x, y, *, strict = True): def permission(permission_name, allow = True): overwrite = discord.PermissionOverwrite() - if permission_name not in overwrite.VALID_NAMES: + if permission_name not in getattr(overwrite, 'VALID_NAMES'): log.error("Invalid permission name {}!".format(permission_name)) return None args = { permission_name: allow } @@ -711,11 +878,11 @@ async def overwrite_permissions(channel, whom, overwrite, *, strict = True): log.debug('Permissions for {} in {} are already correct.'.format(whom.name, channel.name)) return log.info('Setting permissions for {} in {} to {}'.format(whom.name, channel.name, permissions.__dict__)) - await client.edit_channel_permissions(channel, whom, overwrite) + await channel.set_permissions(whom, overwrite) async def set_role(member, role, minimum = 1): if role is None: - log.warning("Can't find {} role to assign to {}".format(role_name, member.name)) + log.warning("Can't find role to assign to {}".format(member.name)) return if minimum is not None and len(member.roles) > minimum: @@ -726,7 +893,7 @@ async def set_role(member, role, minimum = 1): return log.info('Adding {} role to {}'.format(role.name, member.name)) try: - await client.add_roles(member, role) + await member.add_roles(role) except discord.errors.Forbidden: log.warning('Forbidden to add {} role to {}'.format(role.name, member.name)) diff --git a/cat.py b/cat.py index f33b5f6..f44a9da 100644 --- a/cat.py +++ b/cat.py @@ -2,10 +2,12 @@ import asyncio import bot async def maybe_meow(destination, voice = None, **args): - result = False + result = None if destination is not None: result = await bot.maybe_say(destination, 'meow!', **args) - if result and voice is not None: + if result is not False and voice is not None: + play_args = { k: v for k, v in args.items() } + play_args['name'] = 'meowing sound' await bot.play_sound(voice, 'meow.wav', **args) async def meow(destination, voice = None, **extra): @@ -14,12 +16,20 @@ async def meow(destination, voice = None, **extra): args['probability'] = 1.0 await maybe_meow(destination, voice, **args) -async def purr(destination, voice = None, **args): - result = False +async def maybe_purr(destination, voice = None, **args): + result = None if destination is not None: - result = await bot.say(destination, 'purr') - if result and voice is not None: - await bot.play_sound(voice, 'purr.wav', **args) + result = await bot.maybe_say(destination, 'purr', **args) + if result is not False and voice is not None: + play_args = { k: v for k, v in args.items() } + play_args['name'] = 'purring sound' + await bot.play_sound(voice, 'purr.wav', **play_args) + +async def purr(destination, voice = None, **extra): + args = {} + args.update(**extra) + args['probability'] = 1.0 + await maybe_purr(destination, voice, **args) async def hiss(destination, **args): await bot.say(destination, 'hiss!', **args) diff --git a/db.py b/db.py index 546c24f..08f3c8e 100644 --- a/db.py +++ b/db.py @@ -1,4 +1,4 @@ -import discord_unofficial as discord +import discord import logging import sqlite3 import re @@ -6,6 +6,8 @@ import time import uuid from types import GeneratorType +import bot + log = logging.getLogger('db') class DBConnection(object): @@ -93,26 +95,33 @@ class DBConnection(object): self.commit() self.close_db() - def get_all_from_table(self, client, table, servers = []): + def snowflake_row(self, row): + ret = dict(row) + for k, v in ret.items(): + if k.endswith('_id'): + ret[k] = bot.snowflake(v) + return ret + + def get_all_from_table(self, client, table, servers = [], snowflake = False): params = [client.user.id] sql = 'select * from {} where bot_id=?'.format(table) if len(servers): sql += ' and server_id in ({})'.format(', '.join(['?'] * len(servers))) for server in servers: - if type(server) == discord.server.Server: + if type(server) == discord.guild.Guild: params.append(server.id) else: params.append(server) cursor = self.query(sql, params) for row in cursor.fetchall(): - yield dict(row) + yield self.snowflake_row(row) if snowflake else dict(row) self.close_db() - def get_from_table(self, client, table, id): + def get_from_table(self, client, table, id, snowflake = False): cursor = self.query('select * from {} where bot_id=? and id=?'.format(table), [client.user.id, id]) row = cursor.fetchone() if row: - ret = dict(row) + ret = self.snowflake_row(row) if snowflake else dict(row) else: ret = None self.close_db() @@ -171,15 +180,3 @@ class DBConnection(object): ret = False self.close_db() return ret - - def get_message_servers(self, client, message, allow_private = False): - servers = [] - if message.channel.is_private: - if allow_private: - servers.append('private') - for server in client.servers: - if message.author in server.members: - servers.append(server) - else: - servers = [message.channel.server] - return servers diff --git a/plugins.py b/plugins/__init__.py similarity index 93% rename from plugins.py rename to plugins/__init__.py index bf64d75..4cd28c1 100644 --- a/plugins.py +++ b/plugins/__init__.py @@ -33,7 +33,7 @@ class Plugins(object): def refresh(self): seen = [] - for py in glob.glob('plugin/*/__init__.py') + glob.glob('plugin/*/*.py'): + for py in glob.glob('plugins/*/__init__.py') + glob.glob('plugins/*/*.py'): path = py.split('.py')[0] plugin = os.path.basename(path) name = os.path.basename(os.path.dirname(py)) @@ -72,7 +72,7 @@ class Plugins(object): def valid_commands(self): commands = [] for name in self.plugins(): - plugin_commands = self.call_method(name, 'valid_commands') + plugin_commands = self.call_method(name, 'valid_commands') if self.has_method(name, 'valid_commands') else None if plugin_commands is not None: commands += plugin_commands return sorted(list(set(commands))) @@ -151,10 +151,14 @@ class Plugins(object): # Plugin can return True to force skipping other handlers. async def handle_command(self, message, command, raw): for name in self.method_cache('handle_command'): + commands = [] if self.has_method(name, 'valid_commands'): - if command not in self.call_method(name, 'valid_commands'): - log.debug("Plugin {} doesn't handle {} command.".format(name, command)) - continue + commands += self.call_method(name, 'valid_commands') + if self.has_method(name, 'secret_commands'): + commands += self.call_method(name, 'secret_commands') + if command not in commands: + log.debug("Plugin {} doesn't handle {} command.".format(name, command)) + continue result = await self.call_coroutine(name, 'handle_command', message, command, raw) if result is PluginCommand.ignored: log.debug('Plugin {} ignored command {}.'.format(name, command)) diff --git a/plugin/admin/admin.py b/plugins/admin/__init__.py similarity index 98% rename from plugin/admin/admin.py rename to plugins/admin/__init__.py index e980f84..fbd452f 100644 --- a/plugin/admin/admin.py +++ b/plugins/admin/__init__.py @@ -248,6 +248,7 @@ class Admin(object): allow = False log.info('{} server={} role={} permission={} channel={}'.format(action, where, who, permission, channel)) + permissions = None overwrite = bot.permission(permission, allow) if overwrite is None: await bot.say(message.channel, '{}?'.format(permission)) @@ -277,7 +278,7 @@ class Admin(object): permissions = member.server_permissions elif parsed[0] == '&': # Role. - role_id = parsed[1:] + role_id = bot.snowflake(parsed[1:]) for role in server.roles: if role.id == role_id: target = role @@ -311,7 +312,7 @@ class Admin(object): permissions.update(**args) if parsed[0] == '&': - await client.edit_role(server, target, permissions = permissions) + await target.edit(permissions = permissions) else: await bot.say(message.channel, 'Need channel to update member permissions!') return diff --git a/plugin/affection/affection.py b/plugins/affection/__init__.py similarity index 68% rename from plugin/affection/affection.py rename to plugins/affection/__init__.py index a9fedd9..7f626eb 100644 --- a/plugin/affection/affection.py +++ b/plugins/affection/__init__.py @@ -1,7 +1,9 @@ import asyncio +import random from plugins import PluginCommand import bot +import cat class Affection(object): def __init__(self): @@ -16,26 +18,23 @@ class Affection(object): def secret_commands(self): return self.eat + self.drink + self.drugs + self.fuss - @asyncio.coroutine - def handle_command(self, message, command, raw): + async def handle_command(self, message, command, raw): if command not in self.secret_commands(): return PluginCommand.ignored - result = yield from self.handle_affection(message, command) + result = await self.handle_affection(message, command) if result: return PluginCommand.exclusive else: return PluginCommand.handled - @asyncio.coroutine - def handle_help(self, message, command, *args): + async def handle_help(self, message, command, *args): if len(args): if command in self.secret_commands(): - yield from self.help_affection(message, *args) + await self.help_affection(message, *args) return PluginCommand.handled return PluginCommand.ignored - @asyncio.coroutine - def handle_affection(self, message, command): + async def handle_affection(self, message, command): if command in self.eat: responses = ['om nom nom', '=^-^=', 'meow!', 'lick', ':cat:', ':cat2:', ':fork_and_knife:', ':smiley_cat:'] elif command in self.drink: @@ -44,17 +43,15 @@ class Affection(object): responses = ['>(@.@)<'] else: responses = ['nuzzle', '=^-^=', 'meow!', 'mrrp', ':cat:', ':cat2:', ':paw_prints:', ':smiley_cat:'] - for response in responses: - said = yield from bot.maybe_say(message.channel, response) - if said: - yield from bot.maybe_play_sound(bot.voice_channel_for_user(message.author), 'purr.wav', join = False) - return True + said = await bot.maybe_say(message.channel, random.choice(responses)) + if said: + await cat.maybe_purr(None, voice = bot.voice_channel_for_user(message.author), join = False) + return True return False - @asyncio.coroutine - def help_affection(self, message, args): + async def help_affection(self, message, args): lines = [ "You can give me a treat if you like. I may (or may not) show gratitude.", 'Probably not.' ] - yield from bot.say_lines(message.channel, many) + await bot.say_many(message.channel, lines) diff --git a/plugin/announcements/announcements.py b/plugins/announcements/__init__.py similarity index 97% rename from plugin/announcements/announcements.py rename to plugins/announcements/__init__.py index fa7f47d..0ffb306 100644 --- a/plugin/announcements/announcements.py +++ b/plugins/announcements/__init__.py @@ -14,7 +14,11 @@ import cat class Announcement(object): def __init__(self, row): for k, v in dict(row).items(): - setattr(self, k, v) + canon = v + if k.endswith('_id'): + if k != 'channel_id' or v != 'private': + canon = bot.snowflake(v) + setattr(self, k, canon) if not hasattr(self, 'message_limit'): self.message_limit = None self.is_private = hasattr(self, 'channel_id') and self.channel_id == 'private' @@ -143,7 +147,7 @@ class Announcements(DBConnection): log.debug('Anyone can show own announcements.') return announcement if not announcement.is_private: - server = client.get_server(announcement.server_id) + server = bot.get_server(announcement.server_id) if server is not None: member = server.get_member(author.id) if member in server.members: @@ -156,7 +160,7 @@ class Announcements(DBConnection): log.debug('Anyone can manage own announcements.') return announcement if not announcement.is_private: - server = client.get_server(announcement.server_id) + server = bot.get_server(announcement.server_id) announcer = server.get_member(announcement.member_id) if announcer is None: announcer = server.me @@ -165,7 +169,7 @@ class Announcements(DBConnection): log.debug('Member {} with role {} on server {} can edit announcements from lower role.'.format(member.name, member.top_role.name, server.name)) return announcement elif command == 'asap' or member.top_role == announcer.top_role: - if member.top_role >= member.server.me.top_role: + if member.top_role >= member.guild.me.top_role: log.debug('Member {} with role {} on server {} can edit announcements.'.format(member.name, member.top_role.name, server.name)) return announcement @@ -173,7 +177,7 @@ class Announcements(DBConnection): if announcement.is_private: log.debug('Anyone can create a private announcement.') return announcement - server = client.get_server(announcement.server_id) + server = bot.get_server(announcement.server_id) member = server.get_member(announcement.member_id) if bot.lower_role_than(member, strict = True): log.debug('Member {} with role {} on server {} can create announcements.'.format(member.name, member.top_role.name, server.name)) @@ -300,14 +304,14 @@ class Announcements(DBConnection): return results = [] - for announcement in self.get_all_announcements(client, self.get_message_servers(client, message, True)): + for announcement in self.get_all_announcements(client, bot.get_message_servers(message, True)): if announcement.is_private and not announcement.owned_by(message.author): continue text = 'announcement **{}**'.format(announcement.id) if announcement.is_private: text += ' in private' else: - server = client.get_server(announcement.server_id) + server = bot.get_server(announcement.server_id) member = server.get_member(announcement.member_id) text += ' by {}'.format(member.name) if announcement.channel_id: @@ -339,13 +343,13 @@ class Announcements(DBConnection): lines = [] if announcement.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(announcement.member_id) except: log.exception("announce") else: - server = client.get_server(announcement.server_id) + server = bot.get_server(announcement.server_id) member = server.get_member(announcement.member_id) text = '**{}announcement {} by {}'.format('private ' if announcement.is_private else '', id, member.name if member is not None else announcement.member_id) now = int(time.time()) @@ -383,7 +387,7 @@ class Announcements(DBConnection): text += ' every {}'.format(bot.unparse_seconds(int(announcement.interval))) mention = announcement.mention if mention: - if message.channel.is_private and mention[0] == '&': + if bot.is_private(message.channel) and mention[0] == '&': # Can't mention a role in a private channel. text += ' tell @{}'.format(mention) else: @@ -524,7 +528,7 @@ class Announcements(DBConnection): elif param == 'here': if arg != 'in': break - if message.channel.is_private: + if bot.is_private(message.channel): parsed[k] = 'private' ok = True else: @@ -615,14 +619,14 @@ class Announcements(DBConnection): if not channel: await bot.say(message.channel, 'Invalid channel!') return None - server = channel.server + server = channel.guild parsed['server_id'] = server.id elif 'voice_id' in parsed: voice = bot.parse_channel(parsed['voice_id'], author = message.author, text_ok = False) if not voice: await bot.say(message.channel, 'Invalid voice channel!') return None - server = voice.server + server = voice.guild parsed['server_id'] = server.id if 'start_date' in parsed and 'end_date' in parsed: @@ -808,7 +812,7 @@ class Announcements(DBConnection): channel = None if announcement.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(announcement.member_id) channel = await client.start_private_message(member) @@ -845,7 +849,7 @@ class Announcements(DBConnection): cutoff = datetime.datetime.utcnow() - datetime.timedelta(0, interval) log.debug('Getting logs from {}.'.format(cutoff)) logs = [] - async for message in client.logs_from(channel, after = cutoff): + async for message in channel.history(after = cutoff): logs.append(message) # Don't spam the same message in a quiet channel even if it hasn't # been posted since the cutoff. @@ -853,7 +857,7 @@ class Announcements(DBConnection): if len(logs) < message_limit: log.debug('Fewer than {} messages ({}).'.format(message_limit, len(logs))) logs = [] - async for message in client.logs_from(channel, limit = message_limit): + async for message in channel.history(limit = message_limit): logs.append(message) for message in logs: raw = re.sub(r'^( 1: - result = yield from self.get_message(message, *args) + result = await self.get_message(message, *args) if result is not None: - yield from bot.say(message.channel, bot.digest(result.content)) + await bot.say(message.channel, bot.digest(result.content)) return - yield from cat.yelp(message.channel) + await cat.yelp(message.channel) - @asyncio.coroutine - def handle_id(self, message, args): + async def handle_id(self, message, args): do = Identify.member.value | Identify.role.value | Identify.channel.value | Identify.server.value target = None channel_id = None @@ -153,7 +146,7 @@ class Developer(object): try: parsed = ap.parse_args(args) except ArgumentParserError: - yield from cat.yelp(message.channel) + await cat.yelp(message.channel) return if parsed.target: @@ -172,7 +165,7 @@ class Developer(object): channel_id = str(id) do = Identify.channel.value elif re.match(r'^\d+$', parsed.target): - server_id = str(target) + server_id = bot.snowflake(target) else: target = parsed.target elif parsed.channel or parsed.member or parsed.role or parsed.server: @@ -188,7 +181,7 @@ class Developer(object): results = [] if do & Identify.member.value: - for server in client.servers: + for server in client.guilds: if member_id: member = server.get_member(member_id) if member is not None: @@ -204,13 +197,13 @@ class Developer(object): results.append(member) if do & Identify.role.value: if role_id: - for server in client.servers: + for server in client.guilds: for role in server.roles: if role.id == role_id: results.append(role) break else: - for server in client.servers: + for server in client.guilds: for role in server.roles: if target and role.name.lower() != target.lower(): continue @@ -227,13 +220,13 @@ class Developer(object): results.append(channel) if do & Identify.server.value: if server_id: - for server in client.servers: + for server in client.guilds: if server.id == server_id: results.append(server) else: - for server in client.servers: + for server in client.guilds: if target and server.name.lower() != target.lower(): continue results.append(server) - yield from bot.say_many(message.channel, ['{} {} {}{}'.format(Identify.string(result), result.id, result.name, ' {}'.format(result.joined_at) if type(result) == discord.member.Member else '') for result in set(results) if bot.shared_with(message.author, result, allow_admin_override = bool(parsed.admin))]) + await bot.say_many(message.channel, ['{} {} {}{}'.format(Identify.string(result), result.id, result.name, ' {}'.format(result.joined_at) if type(result) == discord.member.Member else '') for result in set(results) if bot.shared_with(message.author, result, allow_admin_override = bool(parsed.admin))]) diff --git a/plugin/edts/__init__.py b/plugins/edts/__init__.py similarity index 92% rename from plugin/edts/__init__.py rename to plugins/edts/__init__.py index 173d293..7455ead 100644 --- a/plugin/edts/__init__.py +++ b/plugins/edts/__init__.py @@ -35,7 +35,7 @@ class Edts(object): await self.wait_for_prompt() if self.edi.before: log.debug(self.edi.before) - await bot.say(message.channel, '{}```{}```'.format('{} '.format(message.author.mention) if not message.channel.is_private else '', self.edi.before)) + await bot.say(message.channel, '{}```{}```'.format('{} '.format(message.author.mention) if not bot.is_private(message.channel) else '', self.edi.before)) else: await bot.say(message.channel, 'Sorry!') return PluginCommand.exclusive diff --git a/plugin/faction/faction.py b/plugins/faction/__init__.py similarity index 97% rename from plugin/faction/faction.py rename to plugins/faction/__init__.py index 98f3a15..97f848f 100644 --- a/plugin/faction/faction.py +++ b/plugins/faction/__init__.py @@ -23,18 +23,18 @@ class TrackedFaction(object): for k, v in dict(row).items(): setattr(self, k, v) if hasattr(self, 'newsflash_channel_id'): - self.is_private = hasattr(self, 'newsflash_channel_id') and self.newsflash_channel_id == 'private' + self.is_private = hasattr(self, 'newsflash_channel_id') and getattr(self, 'newsflash_channel_id') == 'private' else: self.newsflash_channel_id = None - self.is_private = hasattr(self, 'channel_id') and self.channel_id == 'private' + self.is_private = hasattr(self, 'channel_id') and getattr(self, 'channel_id') == 'private' def owned_by(self, owner): try: # owner is a User object. - return self.member_id == owner.id + return getattr(self, 'member_id') == owner.id except AttributeError: # owner is a string. - return self.member_id == owner + return getattr(self, 'member_id') == owner class TrackedSystem(TrackedFaction): def __init__(self, row): @@ -74,7 +74,7 @@ class Faction(DBConnection): self.close_db() def get_all_tracked_factions(self, servers = []): - for row in self.get_all_from_table(client, 'factions', servers): + for row in self.get_all_from_table(client, 'factions', servers, snowflake = True): yield TrackedFaction(row) def get_tracked_faction(self, id): @@ -98,11 +98,11 @@ class Faction(DBConnection): return self.delete_from_table(client, 'factions', id) def get_all_tracked_systems(self, servers = []): - for row in self.get_all_from_table(client, 'systems', servers): + for row in self.get_all_from_table(client, 'systems', servers, snowflake = True): yield TrackedSystem(row) def get_tracked_system(self, id): - row = self.get_from_table(client, 'systems', id) + row = self.get_from_table(client, 'systems', id, snowflake = True) return TrackedSystem(row) if row else None def get_tracked_systems(self): @@ -417,7 +417,7 @@ class Faction(DBConnection): return results = [] - servers = self.get_message_servers(client, message, True) + servers = bot.get_message_servers(message, True) for tracked in self.get_all_tracked_factions(servers) if what == 'faction' else self.get_all_tracked_systems(servers): if tracked.is_private and not tracked.owned_by(message.author): continue @@ -425,14 +425,18 @@ class Faction(DBConnection): if tracked.is_private: text += ' in private' else: - server = client.get_server(tracked.server_id) + server = bot.get_server(tracked.server_id) member = server.get_member(tracked.member_id) - text += ' by {}'.format(member.name) - text += ' in <#{}>'.format(tracked.channel_id) + if member is not None: + text += ' by {}'.format(member.name) + if tracked.channel_id: + text += ' in <#{}>'.format(tracked.channel_id) if tracked.newsflash_channel_id: text += ' news <#{}>'.format(tracked.newsflash_channel_id) if what == 'faction': - text += ' last state {} at {}'.format(eddb.state_name(tracked.state_id), bot.iso8601(tracked.updated)) + text += ' last state {}'.format(eddb.state_name(tracked.state_id)) + if tracked.updated: + text += ' at {}'.format(bot.iso8601(tracked.updated)) results.append(text) if len(results): @@ -457,13 +461,13 @@ class Faction(DBConnection): lines = [] if tracked.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(tracked.member_id) except: log.exception(what) else: - server = client.get_server(tracked.server_id) + server = bot.get_server(tracked.server_id) member = server.get_member(tracked.member_id) text = '**{} tracking {} {} by {}'.format(id, what, tracked.name, member.name if member is not None else tracked.member_id) now = self.now() @@ -583,15 +587,15 @@ class Faction(DBConnection): channel = None if tracked.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(tracked.member_id) - channel = await client.start_private_message(member) + channel = await member.create_dm() break except: log.exception('report_tracked_faction') else: - channel = client.get_channel(tracked.channel_id) + channel = bot.get_channel(tracked.channel_id) if channel is not None: result = await self.get_faction(None, tracked.name) @@ -617,15 +621,15 @@ class Faction(DBConnection): channel = None if tracked.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(tracked.member_id) - channel = await client.start_private_message(member) + channel = await member.create_dm() break except: log.exception('report_tracked_system') else: - channel = client.get_channel(tracked.channel_id) + channel = bot.get_channel(tracked.channel_id) if channel is not None: result = await self.get_system(None, tracked.name) @@ -655,15 +659,15 @@ class Faction(DBConnection): channel = None channel_id = tracked.newsflash_channel_id if tracked.channel_id is None else tracked.channel_id if tracked.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(tracked.member_id) - channel = await client.start_private_message(member) + channel = await member.create_dm() break except: log.exception('report_tracked_{}'.format(what)) else: - channel = client.get_channel(channel_id) + channel = bot.get_channel(channel_id) if channel is None: log.warning("Can't get channel for {} {}".format(what, tracked.id)) @@ -761,7 +765,7 @@ class Faction(DBConnection): log.debug('Anyone can show own {}s.'.format(what)) return tracked if not tracked.is_private: - server = client.get_server(tracked.server_id) + server = bot.get_server(tracked.server_id) if server is not None: member = server.get_member(author.id) if member in server.members: @@ -774,14 +778,14 @@ class Faction(DBConnection): log.debug('Anyone can manage own {}s.', what) return tracked if not tracked.is_private: - server = client.get_server(tracked.server_id) + server = bot.get_server(tracked.server_id) announcer = server.get_member(tracked.member_id) member = server.get_member(author.id) if member.top_role > announcer.top_role: log.debug('Member {} on server {} can edit {}s from lower role.'.format(member.name, server.name, what)) return tracked elif member.top_role == announcer.top_role: - if member.top_role >= member.server.me.top_role: + if member.top_role >= member.guild.me.top_role: log.debug('Member {} with role {} on server {} can edit {}s.'.format(member.name, member.top_role.name, server.name, what)) return tracked @@ -789,9 +793,8 @@ class Faction(DBConnection): if tracked.is_private: log.debug('Anyone can create a private {}.'.format(what)) return tracked - server = client.get_server(tracked.server_id) + server = bot.get_server(tracked.server_id) member = server.get_member(tracked.member_id) - bot_member = server.get_member(client.user.id) if bot.lower_role_than(member, strict = True): log.debug('Member {} with role {} on server {} can create {}s.'.format(member.name, member.top_role.name, server.name, what)) return tracked @@ -826,10 +829,7 @@ class Faction(DBConnection): parsed = {} name = None - if editing: - command = 'edit' - else: - command = 'create' + if not editing: parsed['member_id'] = message.author.id log.debug('{} track {}'.format(what, args)) @@ -868,7 +868,7 @@ class Faction(DBConnection): ok = True else: parsed[k] = message.channel.id - parsed[s] = message.channel.server.id + parsed[s] = message.channel.guild.id ok = True elif param == 'private': parsed[k] = 'private' @@ -878,7 +878,7 @@ class Faction(DBConnection): channel = bot.parse_channel(param, author = message.author, voice_ok = False) if channel is not None: parsed[k] = channel.id - parsed[s] = channel.server.id + parsed[s] = channel.guild.id ok = True else: break @@ -940,7 +940,7 @@ class Faction(DBConnection): else: channel = bot.parse_channel(parsed['channel_id'], author = message.author, voice_ok = False) newsflash_channel = bot.parse_channel(parsed['newsflash_channel_id'], author = message.author, voice_ok = False) - if channel.server != newsflash_channel.server: + if channel.guild != newsflash_channel.guild: log.info('News flash channel must be on same server.') await bot.say(message.channel, 'Flash channel server!') return None @@ -1128,15 +1128,15 @@ class Faction(DBConnection): async def remember_channel_for_news_flash(self, system, tracked, announce): channel = None if tracked.is_private: - for server in client.servers: + for server in client.guilds: try: member = server.get_member(tracked.member_id) - channel = await client.start_private_message(member) + channel = await member.create_dm() break except: pass else: - channel = client.get_channel(tracked.newsflash_channel_id) + channel = bot.get_channel(tracked.newsflash_channel_id) if channel is not None: channels = announce.get(system, set()) channels.add(channel) diff --git a/plugin/facts/cat_haiku.txt b/plugins/facts/cat_haiku.txt similarity index 100% rename from plugin/facts/cat_haiku.txt rename to plugins/facts/cat_haiku.txt diff --git a/plugin/facts/catfacts.txt b/plugins/facts/catfacts.txt similarity index 100% rename from plugin/facts/catfacts.txt rename to plugins/facts/catfacts.txt diff --git a/plugin/facts/facts.py b/plugins/facts/facts.py similarity index 95% rename from plugin/facts/facts.py rename to plugins/facts/facts.py index 33b555c..3ad5822 100644 --- a/plugin/facts/facts.py +++ b/plugins/facts/facts.py @@ -7,7 +7,7 @@ import bot class Facts(object): def __init__(self): - self.path = os.path.dirname(__file__)[(len(os.getcwd()) + 1):] + self.path = os.path.split(__file__)[0] with open(os.sep.join([self.path, 'catfacts.txt']), 'r') as fd: self.catFacts = fd.readlines() with open(os.sep.join([self.path, 'cat_haiku.txt']), 'r') as fd: diff --git a/plugin/feeds/feeds.py b/plugins/feeds/__init__.py similarity index 95% rename from plugin/feeds/feeds.py rename to plugins/feeds/__init__.py index 9c4f987..e247df0 100644 --- a/plugin/feeds/feeds.py +++ b/plugins/feeds/__init__.py @@ -1,6 +1,6 @@ import asyncio import bs4 -import discord_unofficial as discord +import discord import feedparser import json import math @@ -32,7 +32,7 @@ class Feeds(DBConnection): self.dbh.commit() def get_all_feeds(self, client, servers = []): - for row in self.get_all_from_table(client, 'feeds', servers): + for row in self.get_all_from_table(client, 'feeds', servers, snowflake = True): yield row def get_feeds(self, client): @@ -70,34 +70,38 @@ class Feeds(DBConnection): return k async def set_rss_permissions(self, channel_id): - channel = client.get_channel(channel_id) + channel = bot.get_channel(channel_id) if channel is None: log.warning("Can't get channel {} for RSS feeds.".format(channel_id)) return role = None # My highest role. - role = channel.server.me.top_role + role = channel.guild.me.top_role if role is not None: # Permissions for bot. overwrite = discord.PermissionOverwrite() - overwrite.read_messages = True - overwrite.read_message_history = True - overwrite.send_messages = True - overwrite.send_tts_messages = True - overwrite.manage_messages = True - overwrite.attach_files = True + overwrite.update( + read_messages = True, + read_message_history = True, + send_messages = True, + send_tts_messages = True, + manage_messages = True, + attach_files = True + ) await bot.overwrite_permissions(channel, role, overwrite, strict = False) # Permissions for @everyone. overwrite = discord.PermissionOverwrite() - overwrite.read_messages = True - overwrite.read_message_history = True - overwrite.send_messages = False - overwrite.send_tts_messages = False - await bot.overwrite_permissions(channel, channel.server.default_role, overwrite) + overwrite.update( + read_messages = True, + read_message_history = True, + send_messages = False, + send_tts_messages = False + ) + await bot.overwrite_permissions(channel, channel.guild.default_role, overwrite) async def do_rss(self, feeds): - channel_id = str(feeds[0]['channel_id']) - channel = client.get_channel(channel_id) + channel_id = feeds[0]['channel_id'] + channel = bot.get_channel(channel_id) if not channel: log.warning("Can't get channel {} for feed.".format(channel_id)) return @@ -156,7 +160,7 @@ class Feeds(DBConnection): limit = max(len(all_entries) * 2, int(bot.get('message_limit') * 1.5)) digests = [] - async for message in client.logs_from(channel, limit = limit): + async for message in channel.history(limit = limit): digest = bot.digest(message.content) log.debug('Saw previously posted RSS with digest {}'.format(digest)) if digest not in digests: @@ -295,7 +299,7 @@ class Feeds(DBConnection): # Anyone on the server can show details of a feed. if command == 'show': if feed is not None: - server = client.get_server(feed['server_id']) + server = bot.get_server(feed['server_id']) if server is not None: member = server.get_member(author.id) if member in server.members: @@ -304,14 +308,14 @@ class Feeds(DBConnection): if command in ['create', 'edit', 'delete', 'schedule']: if feed is not None: - server = client.get_server(feed['server_id']) + server = bot.get_server(feed['server_id']) member = server.get_member(author.id) if bot.lower_role_than(member): log.debug('Member {} with role {} on server {} can manage feeds.'.format(member.name, member.top_role, server.name)) return feed if command in ['time']: - if bot.is_admin(member, member.server): + if bot.is_admin(member, member.guild): return True await cat.hiss(channel) @@ -323,7 +327,7 @@ class Feeds(DBConnection): return results = [] - for feed in self.get_all_feeds(client, self.get_message_servers(client, message)): + for feed in self.get_all_feeds(client, bot.get_message_servers(message)): text = '{} **{}** url {} in <#{}>'.format(feed['description'], feed['id'], feed['url'], feed['channel_id']) results.append(text) @@ -479,11 +483,11 @@ class Feeds(DBConnection): return None if 'channel_id' in parsed: - channel = client.get_channel(parsed['channel_id']) + channel = bot.get_channel(parsed['channel_id']) if not channel: await bot.say(message.channel, 'Invalid channel!') return None - server = channel.server + server = channel.guild parsed['server_id'] = server.id if editing: diff --git a/plugin/greetings/greetings.py b/plugins/greetings/__init__.py similarity index 93% rename from plugin/greetings/greetings.py rename to plugins/greetings/__init__.py index 0c2a822..c4b1e00 100644 --- a/plugin/greetings/greetings.py +++ b/plugins/greetings/__init__.py @@ -1,5 +1,5 @@ import asyncio -import discord_unofficial as discord +import discord import re import shlex @@ -26,12 +26,12 @@ class Greetings(DBConnection): async def on_ready(self): for greeting in self.get_greetings(client): - channel = client.get_channel(greeting['channel_id']) + channel = bot.get_channel(greeting['channel_id']) if channel is None: log.warning("Can't get channel for greeting {}!".format(greeting['id'])) continue log.debug('Greeting in {}.'.format(channel.name)) - for member in channel.server.members: + for member in channel.guild.members: result = await self.maybe_greet(member, channel = channel, greeting = greeting) if result: greeting['sound'] = None @@ -46,25 +46,26 @@ class Greetings(DBConnection): if not after.bot: return - return + log.debug('{} status was {} now {}.'.format(bot.canonicalise(after), before.status, after.status)) - log.debug('{}#{} status was {} now {}.'.format(after.name, after.discriminator, before.status, after.status)) + # XXX: Disable excessive hissing at other bots + return if discord.Status.offline not in [before.status, after.status]: return try: servers = [] - for server in client.servers: + for server in client.guilds: if server.get_member(after.id): servers.append(server.id) if not len(servers): - log.debug("{}#{} isn't on any shared servers!".format(after.name, after.discriminator)) + log.debug("{} isn't on any shared servers!".format(bot.canonicalise(after))) return for channel_id in [greeting['channel_id'] for greeting in list(self.get_greetings(client))]: - channel = client.get_channel(channel_id) + channel = bot.get_channel(channel_id) if not bot.shared_with(after, channel): - log.debug("{}#{} isn't in channel {} on server {}".format(after.name, after.discriminator, channel.name, server.name)) + log.debug("{} isn't in channel {} on server {}".format(bot.canonicalise(after), channel.name, server.name)) continue await bot.maybe_say(channel, 'hiss! {}'.format(after.mention), probability = 0.1) return @@ -88,13 +89,13 @@ class Greetings(DBConnection): yield greeting def get_greeting(self, client, id): - return self.get_from_table(client, 'greetings', id) + return self.get_from_table(client, 'greetings', id, snowflake = True) def get_greetings(self, client): now = self.now() cursor = self.query("select id, server_id, channel_id, voice_id, sound, message, role_id, maximum from greetings where bot_id=? and enabled='true'", [client.user.id]) for row in cursor.fetchall(): - yield dict(row) + yield self.snowflake_row(row) self.close_db() def create_greeting(self, client, **args): @@ -122,7 +123,7 @@ class Greetings(DBConnection): # Anyone on the server can show details of a greeting. if command == 'show': if greeting is not None: - server = client.get_server(greeting['server_id']) + server = bot.get_server(greeting['server_id']) if server is not None: member = server.get_member(author.id) if member in server.members: @@ -131,7 +132,7 @@ class Greetings(DBConnection): if command in ['create', 'edit', 'delete', 'schedule']: if greeting is not None: - server = client.get_server(greeting['server_id']) + server = bot.get_server(greeting['server_id']) member = server.get_member(author.id) if bot.lower_role_than(member): log.debug('Member {} with role {} on server {} can manage greetings.'.format(member.name, member.top_role.name, server.name)) @@ -146,8 +147,8 @@ class Greetings(DBConnection): return results = [] - for greeting in self.get_all_greetings(client, self.get_message_servers(client, message)): - server = client.get_server(greeting['server_id']) + for greeting in self.get_all_greetings(client, bot.get_message_servers(message)): + server = bot.get_server(greeting['server_id']) text = '**{}** in <#{}>'.format(greeting['id'], greeting['channel_id']) if greeting['role_id']: text += ' assign &{}'.format(greeting['role_id']) @@ -254,7 +255,7 @@ class Greetings(DBConnection): elif param == 'here': if arg != 'in': break - if message.channel.is_private: + if bot.is_private(message.channel): log.warning("Can't greet in private channel.") break else: @@ -309,11 +310,11 @@ class Greetings(DBConnection): return None if 'channel_id' in parsed: - channel = client.get_channel(parsed['channel_id']) + channel = bot.get_channel(parsed['channel_id']) if not channel: await bot.say(message.channel, 'Invalid channel!') return None - server = channel.server + server = channel.guild parsed['server_id'] = server.id if 'message' not in parsed: @@ -528,14 +529,14 @@ class Greetings(DBConnection): channels = [channel] if channel is not None else client.get_all_channels() for channel in channels: # Don't greet in private channel. - if channel.is_private: + if bot.is_private(channel): log.debug('Not a channel to greet: {} is private.'.format(channel)) continue for greeting in greetings: # Ensure the greeting applies to this server. - if greeting['server_id'] != member.server.id: - log.debug('Not a server to greet: {}.'.format(member.server.name)) + if greeting['server_id'] != member.guild.id: + log.debug('Not a server to greet: {}.'.format(member.guild.name)) continue # Ensure the greeting applies to this channel. @@ -555,11 +556,11 @@ class Greetings(DBConnection): role = member_role break if role is not None: - log.debug('Not greeting {} with {} role already.'.format(member, role_name)) + log.debug('Not greeting {} with {} role already.'.format(member, role.name)) continue # Don't greet a member who isn't here. - if member not in channel.server.members: + if member not in channel.guild.members: log.debug('Not greeting {} in {}: User not in server memberlist.'.format(member, channel.name)) continue @@ -584,7 +585,7 @@ class Greetings(DBConnection): return True async def greet(self, member, greeting): - channel = client.get_channel(greeting['channel_id']) + channel = bot.get_channel(greeting['channel_id']) if channel is None: log.warning("Can't get channel for greeting {}!".format(greeting['id'])) return @@ -601,14 +602,14 @@ class Greetings(DBConnection): return if 'voice_id' not in greeting: return - voice = client.get_channel(greeting['voice_id']) + voice = bot.get_channel(greeting['voice_id']) if voice is None: log.warning("Can't get voice channel for greeting {}!".format(greeting['id'])) return await bot.play_sound(voice, greeting['sound']) async def set_role(self, member, greeting): - for server in client.servers: + for server in client.guilds: if server.id != greeting['server_id']: continue for role in server.roles: diff --git a/plugin/heading/heading.py b/plugins/heading/__init__.py similarity index 74% rename from plugin/heading/heading.py rename to plugins/heading/__init__.py index 489fe9b..94de5d9 100644 --- a/plugin/heading/heading.py +++ b/plugins/heading/__init__.py @@ -14,32 +14,28 @@ class Heading(object): def valid_commands(self): return ['heading'] - @asyncio.coroutine - def handle_command(self, message, command, raw): + async def handle_command(self, message, command, raw): if command not in self.valid_commands(): return PluginCommand.ignored - yield from self.handle_heading(message, shlex.split(raw)[1:]) + await self.handle_heading(message, shlex.split(raw)[1:]) return PluginCommand.exclusive - @asyncio.coroutine - def handle_help(self, message, command, *args): - yield from self.help_heading(message, *args) + async def handle_help(self, message, command, *args): + await self.help_heading(message, *args) - @asyncio.coroutine - def handle_heading(self, message, args): + async def handle_heading(self, message, args): if not len(args): return command = args[0].lower() if command == 'help': - yield from self.help_heading(message) + await self.help_heading(message) elif command in ['from', 'to']: - yield from self.navigate(message, args) + await self.navigate(message, args) else: - yield from bot.say(message.channel, '?') + await bot.say(message.channel, '?') - @asyncio.coroutine - def help_heading(self, message, *args): + async def help_heading(self, message, *args): lines = [ '{} can assist you with orbital cruise.'.format(client.user.mention), '', @@ -57,10 +53,9 @@ class Heading(object): 'I can calculate the exact heading to fly between your current position and target co-ordinates. Send me a command like this:', '```heading from 44.08 -33.12 to -27.14 -130.95```' ] - yield from bot.say_many(message.channel, lines) + await bot.say_many(message.channel, lines) - @asyncio.coroutine - def navigate(self, message, args): + async def navigate(self, message, args): log.debug('Parsing {}'.format(args)) parsed = {} @@ -87,7 +82,7 @@ class Heading(object): except ValueError: break else: - yield from bot.say(message.channel, 'What is {}?'.format(arg)) + await bot.say(message.channel, 'What is {}?'.format(arg)) return if ok: @@ -97,24 +92,24 @@ class Heading(object): for k in ['from', 'to']: if k not in parsed: - yield from bot.say(message.channel, '{} where?'.format(k)) + await bot.say(message.channel, '{} where?'.format(k)) return None if not ok: log.info('Failed to parse heading. Got: {}'.format(parsed)) - yield from bot.say(message.channel, '{}?'.format(arg)) + await bot.say(message.channel, '{}?'.format(arg)) return None for k in ['from', 'to']: latitude, longitude = parsed[k] if latitude < -90 or latitude > 90: - yield from bot.say(message.channel, 'Invalid latitude!') + await bot.say(message.channel, 'Invalid latitude!') return if longitude < -180 or longitude > 180: - yield from bot.say(message.channel, 'Invalid longitude!') + await bot.say(message.channel, 'Invalid longitude!') return - yield from bot.say(message.channel, self.calculate_bearing(parsed['from'], parsed['to'])) + await bot.say(message.channel, self.calculate_bearing(parsed['from'], parsed['to'])) def calculate_bearing(self, start, end): log.info('Calculating bearing from {} to {}.'.format(start, end)) @@ -122,6 +117,6 @@ class Heading(object): slat, slong = [math.radians(l) for l in start] elat, elong = [math.radians(l) for l in end] - x = (math.cos(slat) * math.sin(elat)) - (math.sin(slat) * math.cos(elat) * math.cos(elong - slong)); + x = (math.cos(slat) * math.sin(elat)) - (math.sin(slat) * math.cos(elat) * math.cos(elong - slong)) y = math.sin(elong - slong) * math.cos(elat) return int(math.degrees(math.atan2(y, x)) % 360) diff --git a/plugin/mention_hint/mention_hint.py b/plugins/mention_hint/__init__.py similarity index 63% rename from plugin/mention_hint/mention_hint.py rename to plugins/mention_hint/__init__.py index f8e8297..5829857 100644 --- a/plugin/mention_hint/mention_hint.py +++ b/plugins/mention_hint/__init__.py @@ -9,7 +9,7 @@ class Mention_hint(object): return 'Hint how to @mention the bot.' async def not_our_message(self, message): - if re.match(r'\b{}\b'.format(message.server.me.name), message.content): - await bot.maybe_say(message.channel, message.server.me.mention, probability = 0.1, wake = False) + if re.search(r'\b{}\b'.format(message.guild.me.name), message.content): + await bot.maybe_say(message.channel, message.guild.me.mention, probability = 0.1, wake = False) return PluginCommand.handled return PluginCommand.ignored diff --git a/plugin/o7/o7.py b/plugins/o7/__init__.py similarity index 100% rename from plugin/o7/o7.py rename to plugins/o7/__init__.py diff --git a/plugin/play/play.py b/plugins/play/__init__.py similarity index 78% rename from plugin/play/play.py rename to plugins/play/__init__.py index 7e1eab0..153223c 100644 --- a/plugin/play/play.py +++ b/plugins/play/__init__.py @@ -19,18 +19,23 @@ class Play(object): def valid_commands(self): return ['play', 'tts'] + def secret_commands(self): + return ['tts:'] + async def handle_command(self, message, command, raw): - if command not in self.valid_commands(): + if command not in self.valid_commands() and command not in self.secret_commands(): return PluginCommand.ignored if command == 'play': await self.play(message, shlex.split(raw)[1:]) - elif command == 'tts': + elif command in ['tts', 'tts:']: parts = raw.split(':') args = shlex.split(parts[0])[1:] + if not args and command == 'tts:': + args = ['to', 'me'] if len(parts) > 1: text = ':'.join(parts[1:]).strip() - await self.play(message, args, text) - elif args[0] == 'help': + await self.play(message, args, text = text) + elif len(args) > 0 and args[0] == 'help': await self.help_tts(message) else: await bot.say(message.channel, 'what?') @@ -46,12 +51,13 @@ class Play(object): lines = [ 'Play audio from a URL:', '```', - 'play URL', - 'play URL in CHANNEL', - 'play URL to MEMBER', + 'play URL [as ALIAS]', + 'play URL in CHANNEL [as ALIAS]', + 'play URL to MEMBER [as ALIAS]', '```', 'If you do not specify a #channel I will play to the channel you are in.', - "If you specify `to` a user I will play to that user's channel instead." + "If you specify `to` a user I will play to that user's channel instead.", + 'If you specify `as ALIAS` I will report that I am playing that ALIAS instead of the URL. If the ALIAS consists of multiple words you should quote it, eg `as "cool sound"`.' ] await bot.say_many(message.channel, lines) @@ -59,11 +65,12 @@ class Play(object): lines = [ 'Text-to-speech:', '```', - 'tts in CHANNEL [ARGS]: MESSAGE', - 'tts to MEMBER [ARGS]: MESSAGE', + 'tts in CHANNEL [ARGS] [as ALIAS]: MESSAGE', + 'tts to MEMBER [ARGS] [as ALIAS]: MESSAGE', '', 'If you do not specify a #channel I will play to the channel you are in.', "If you specify `to` a user I will play to that user's channel instead." + 'If you specify `as ALIAS` I will report that I am playing that ALIAS instead of _text-to-speech_. If the ALIAS consists of multiple words you should quote it, eg `as "text to speech"`.' '```', 'The `ARGS` to `tts` are one or more of:', '', @@ -73,11 +80,13 @@ class Play(object): '', '`pitch 0-99` default `50`', '', - '`speed 80-450` default `175`' + '`speed 80-450` default `175`', + '' + ] await bot.say_many(message.channel, lines) - async def play(self, message, args, text = None): + async def play(self, message, args, name = None, text = None): if not len(args): await bot.maybe_say(message.channel, 'what?', probability = 0.5) return False @@ -131,6 +140,10 @@ class Play(object): if param is not None: tts[arg] = param ok = True + elif arg == 'as': + if param is not None: + name = param + ok = True elif not text: # arg is lowercase! url = args[i] @@ -153,16 +166,15 @@ class Play(object): await bot.say(message.channel, 'where?') return False - server = voice.server + server = voice.guild member = server.get_member(message.author.id) if bot.is_admin(member, server) or member.top_role >= server.me.top_role: - await bot.wake_up() if text: log.info('Text-to-speech request by {} in {} on {}: {}'.format(message.author, voice.name, server.name, text)) - result = await bot.tts(voice, text, tts) + result = await bot.tts(voice, text, tts, name = name) else: log.info('Sound {} requested by {} in {} on {}'.format(url, message.author, voice.name, server.name)) - result = await bot.play_sound(voice, url) + result = await bot.play_sound(voice, url, name = name) if not result: await cat.yelp(message.channel) else: diff --git a/plugin/prune/__init__.py b/plugins/prune/__init__.py similarity index 98% rename from plugin/prune/__init__.py rename to plugins/prune/__init__.py index f7bed65..02ac61e 100644 --- a/plugin/prune/__init__.py +++ b/plugins/prune/__init__.py @@ -131,10 +131,10 @@ class Prune(object): await bot.say(message.channel, 'Role?') return None if 'server' not in parsed: - if message.channel.is_private: + if bot.is_private(message.channel): await bot.say(message.channel, 'Which server?') return None - parsed['server'] = message.channel.server + parsed['server'] = message.guild elif parsed['server'] is None: await bot.say(message.channel, 'Server?') return None @@ -175,6 +175,7 @@ class Prune(object): await cat.yelp(message.channel) if pruned is not None: await bot.say(message.channel, str(pruned)) + async def find_members(self, days = None, kick = False, only = False, role = None, server = None): cutoff = datetime.datetime.utcnow() - datetime.timedelta(days) diff --git a/plugin/rng/rng.py b/plugins/rng/__init__.py similarity index 100% rename from plugin/rng/rng.py rename to plugins/rng/__init__.py diff --git a/plugin/role/role.py b/plugins/role/__init__.py similarity index 88% rename from plugin/role/role.py rename to plugins/role/__init__.py index 6a9360c..3ecbe53 100644 --- a/plugin/role/role.py +++ b/plugins/role/__init__.py @@ -1,5 +1,5 @@ import asyncio -import discord_unofficial as discord +import discord import shlex from db import DBConnection @@ -10,7 +10,10 @@ import cat class ManagedRole(object): def __init__(self, row): for k, v in dict(row).items(): - setattr(self, k, v) + if k.endswith('_id'): + setattr(self, k, bot.snowflake(v)) + else: + setattr(self, k, v) if not hasattr(self, 'above_role_id'): self.above_role_id = None if not hasattr(self, 'below_role_id'): @@ -141,13 +144,13 @@ class Role(DBConnection): params.append(role.id) cursor = self.query(sql, params) for row in cursor.fetchall(): - yield ManagedRole(dict(row)) + yield ManagedRole(self.snowflake_row(row)) self.close_db() def get_managed_role(self, role): managed_roles = list(self.get_managed_roles(role)) if managed_roles is None or not len(managed_roles): - log.info('Role {} on {} is not managed'.format(role.name, role.server.name)) + log.info('Role {} on {} is not managed'.format(role.name, role.guild.name)) return None return managed_roles[0] @@ -158,7 +161,7 @@ class Role(DBConnection): await bot.say(message.channel, '?') return (None, None, None) - if role.is_everyone: + if bot.is_everyone(role): log.info("Can't make &everyone assignable!") await bot.say(message.channel, 'everyone?') return (None, None, None) @@ -170,29 +173,29 @@ class Role(DBConnection): if managed_role is None: return (None, None, cat.yelp) - member = role.server.get_member(message.author.id) + member = role.guild.get_member(message.author.id) can_manage = True if command is not None: - can_manage = await self.can_manage_roles(message.author, message.channel, command, role.server) + can_manage = await self.can_manage_roles(message.author, message.channel, command, role.guild) if managed_role.above_role_id: - above_role = self.get_role_on_server(managed_role.above_role_id, role.server) + above_role = self.get_role_on_server(managed_role.above_role_id, role.guild) if not above_role: - log.warning("Can't find above role {} on server {}".format(managed_role.above_role_id, role.server.name)) + log.warning("Can't find above role {} on server {}".format(managed_role.above_role_id, role.guild.name)) return (None, None, cat.yelp) if command is None and member.top_role <= above_role: if member.top_role < above_role or not above_inclusive: - log.info('Member {} needs at least role {} to assign {} on {}'.format(member.name, above_role.name, role.name, role.server.name)) + log.info('Member {} needs at least role {} to assign {} on {}'.format(member.name, above_role.name, role.name, role.guild.name)) return (role, managed_role, cat.hiss) if managed_role.below_role_id: - below_role = self.get_role_on_server(managed_role.below_role_id, role.server) + below_role = self.get_role_on_server(managed_role.below_role_id, role.guild) if not below_role: - log.warning("Can't find below role {} on server {}".format(managed_role.below_role_id, role.server.name)) + log.warning("Can't find below role {} on server {}".format(managed_role.below_role_id, role.guild.name)) return (None, None, cat.yelp) if command is None and member.top_role >= below_role: if member.top_role > below_role or not below_inclusive: - log.info('Member {} needs at least role {} to assign {} on {}'.format(member.name, below_role.name, role.name, role.server.name)) + log.info('Member {} needs at least role {} to assign {} on {}'.format(member.name, below_role.name, role.name, role.guild.name)) return (role, managed_role, cat.hiss) log.debug('Managing role {} for {}'.format(role.name, member.name)) @@ -202,7 +205,7 @@ class Role(DBConnection): roles = [] hiss = False for managed_role in self.get_managed_roles(): - server = client.get_server(managed_role.server_id) + server = bot.get_server(managed_role.server_id) result = await self.can_manage_roles(message.author, message.channel, 'list', server, False) if not result: hiss = True @@ -211,9 +214,9 @@ class Role(DBConnection): log.debug('Not showing managed role {} on server {} to user {} not on that server'.format(managed_role.role_id, server.name, message.author.name)) hiss = True continue - if not message.channel.is_private: - if message.channel.server.id != managed_role.server_id: - log.debug('Not showing managed role {} on server {} to authorised user {} in public channel {} on {}'.format(managed_role.role_id, server_name, message.author.name, message.channel.name, message.channel.server.name)) + if not bot.is_private(message.channel): + if message.channel.guild.id != managed_role.server_id: + log.debug('Not showing managed role {} on server {} to authorised user {} in public channel {} on {}'.format(managed_role.role_id, server_name, message.author.name, message.channel.name, message.channel.guild.name)) continue role = self.get_role_on_server(managed_role.role_id, server) if not role: @@ -222,7 +225,7 @@ class Role(DBConnection): roles.append(role) if len(roles): - await bot.say_many(message.channel, ['"{}" on {}'.format(role.name, role.server.name) if message.channel.is_private else role.name for role in roles]) + await bot.say_many(message.channel, ['"{}" on {}'.format(role.name, role.guild.name) if bot.is_private(message.channel) else role.name for role in roles]) elif hiss: await cat.hiss(message.channel) else: @@ -239,15 +242,15 @@ class Role(DBConnection): log.error("Can't find role {} to manage!".format(args[0])) await cat.yelp(message.channel) return - result = await self.can_manage_roles(message.author, message.channel, 'manage', role.server) + result = await self.can_manage_roles(message.author, message.channel, 'manage', role.guild) if not result: return - if role.is_everyone: + if bot.is_everyone(role): await bot.say(message.channel, "Can't manage everyone!") return parsed = { - 'server_id': role.server.id, + 'server_id': role.guild.id, 'role_id': role.id } @@ -270,7 +273,7 @@ class Role(DBConnection): if arg == 'above': if param: - above_role = await bot.parse_role(param, author = message.author, server = role.server) + above_role = await bot.parse_role(param, author = message.author, server = role.guild) if above_role: id = above_role.id if inclusive: @@ -279,18 +282,18 @@ class Role(DBConnection): parsed['below_inclusive'] = True else: seen['above'] = True - if not above_role.is_everyone: + if not bot.is_everyone(above_role): parsed['above_role_id'] = id ok = True inclusive = False elif arg == 'below': if param: - below_role = await bot.parse_role(param, author = message.author, server = role.server) + below_role = await bot.parse_role(param, author = message.author, server = role.guild) if below_role: id = below_role.id if inclusive: seen['not-below'] = True - if not above_role.is_everyone: + if not bot.is_everyone(above_role): parsed['above_role_id'] = id parsed['above_inclusive'] = True else: @@ -331,12 +334,12 @@ class Role(DBConnection): if self.update_table(client, 'roles', **parsed): await bot.say(message.channel, role.name) else: - log.error('Failed to update manage role {} on {}'.format(role.name, role.server.name)) + log.error('Failed to update manage role {} on {}'.format(role.name, role.guild.name)) await cat.yelp(message.channel) elif self.insert_into_table(client, 'roles', **parsed): await bot.say(message.channel, role.name) else: - log.error('Failed to manage role {} on {}'.format(role.name, role.server.name)) + log.error('Failed to manage role {} on {}'.format(role.name, role.guild.name)) await cat.yelp(message.channel) async def unmanage_role(self, message, args): @@ -350,7 +353,7 @@ class Role(DBConnection): if not role: return - result = await self.can_manage_roles(message.author, message.channel, 'unmanage', role.server) + result = await self.can_manage_roles(message.author, message.channel, 'unmanage', role.guild) if not result: return @@ -369,21 +372,21 @@ class Role(DBConnection): if not role: return - result = await self.can_manage_roles(message.author, message.channel, 'assign', role.server) + result = await self.can_manage_roles(message.author, message.channel, 'assign', role.guild) if not result: return - member = role.server.get_member(message.author.id) + member = role.guild.get_member(message.author.id) if role in member.roles: - log.info('Member {} already has role {} on {}'.format(member.name, role.name, role.server.name)) + log.info('Member {} already has role {} on {}'.format(member.name, role.name, role.guild.name)) return try: - await client.add_roles(member, role) - log.info('Added role {} to {} on {}'.format(role.name, role.server.name, member.name)) + await member.add_roles(role, reason = 'Self-assignable role') + log.info('Added role {} to {} on {}'.format(role.name, role.guild.name, member.name)) await cat.purr(message.channel) except: - log.error("Couldn't add role {} on {} to {}".format(role.name, role.server.name, member.name)) + log.error("Couldn't add role {} on {} to {}".format(role.name, role.guild.name, member.name)) await cat.yelp(message.channel) async def unassign_role(self, message, args): @@ -397,20 +400,20 @@ class Role(DBConnection): if not role: return - result = await self.can_manage_roles(message.author, message.channel, 'unassign', role.server) + result = await self.can_manage_roles(message.author, message.channel, 'unassign', role.guild) if not result: return - member = role.server.get_member(message.author.id) + member = role.guild.get_member(message.author.id) if role not in member.roles: - log.info("Member {} doesn't have role {} on {}".format(member.name, role.name, role.server.name)) + log.info("Member {} doesn't have role {} on {}".format(member.name, role.name, role.guild.name)) return try: - await client.remove_roles(member, role) - log.info('Removed role {} from {} on {}'.format(role.name, role.server.name, member.name)) + await member.remove_roles(role, reason = 'Self-assignable role') + log.info('Removed role {} from {} on {}'.format(role.name, role.guild.name, member.name)) await cat.purr(message.channel) except: - log.error("Couldn't remove role {} on {} from {}".format(role.name, role.server.name, member.name)) + log.error("Couldn't remove role {} on {} from {}".format(role.name, role.guild.name, member.name)) await cat.yelp(message.channel) diff --git a/plugin/shopping/shopping.py b/plugins/shopping/__init__.py similarity index 85% rename from plugin/shopping/shopping.py rename to plugins/shopping/__init__.py index 477436d..0193697 100644 --- a/plugin/shopping/shopping.py +++ b/plugins/shopping/__init__.py @@ -12,7 +12,7 @@ import eddb class Shopping(object): def __init__(self): - self.path = os.path.dirname(__file__)[(len(os.getcwd()) + 1):] + self.path = os.path.split(__file__)[0] self.COMMODITIES = os.sep.join([self.path, 'commodities.json']) self.COMMODITY_KEYS = ['id', 'average_price', 'is_rare', 'name'] self.MODULES = os.sep.join([self.path, 'modules.json']) @@ -26,15 +26,13 @@ class Shopping(object): def description(self): return 'Advise where to buy ships, modules or commodities.' - @asyncio.coroutine - def handle_command(self, message, command, raw): + async def handle_command(self, message, command, raw): if command not in self.valid_commands(): return PluginCommand.ignored - yield from self.handle_shopping(message, shlex.split(raw)[1:]) + await self.handle_shopping(message, shlex.split(raw)[1:]) return PluginCommand.exclusive - @asyncio.coroutine - def handle_help(self, message, *args): + async def handle_help(self, message, *args): lines = [ '{} can help you find ships, modules and commodities.'.format(client.user.mention), '```shopping [discounted] [large] [orbital] [near SYSTEM] [SHIPS] [MODULES] [COMMODITIES]```', @@ -68,43 +66,51 @@ class Shopping(object): '', 'Example: `shopping near Bugayaman Gold`' ] - yield from bot.say_many(message.channel, lines) + await bot.say_many(message.channel, lines) - @asyncio.coroutine - def handle_shopping(self, message, args): + async def handle_shopping(self, message, args): if not len(args): return if args[0].lower() == 'help': - yield from self.handle_help(message) + await self.handle_help(message) return - yield from client.send_typing(message.channel) - + lines = [] params = {} - names = [] - i = 0 - while i < len(args): - arg = args[i].lower() - log.info(arg) - if arg == 'discounted': - params['discount'] = True - elif arg == 'near': - try: - params['system'] = args[i + 1] - i += 1 - except IndexError: - yield from bot.say(message.channel, 'Where?') - return - elif arg == 'large': - params['large'] = True - elif arg == 'orbital': - params['orbital'] = True - else: - names.append(args[i]) - i += 1 - - yield from self.find_shopping_stations(message, names, **params) + async with message.channel.typing() as typing: + log.info('meow') + names = [] + i = 0 + while i < len(args): + arg = args[i].lower() + log.info(arg) + if arg == 'discounted': + params['discount'] = True + elif arg == 'near': + try: + params['system'] = args[i + 1] + i += 1 + except IndexError: + ret = False + lines = ['Where?'] + break + elif arg == 'large': + params['large'] = True + elif arg == 'orbital': + params['orbital'] = True + else: + names.append(args[i]) + i += 1 + + if not lines: + ret, lines = self.find_shopping_stations(message, names, **params) + + log.info('meow') + if lines: + await bot.say_many(message.channel, lines, typing = typing) + + return ret def filter_key(self, entry, key, value = None): if key is None: @@ -240,16 +246,14 @@ class Shopping(object): break return ships - @asyncio.coroutine - def build_shopping_list(self, message, names): + def build_shopping_list(self, names): commodities = list(self.get_commodities_by_name(names)) modules = list(self.get_modules_by_name(names)) ships = list(self.get_ships_by_name(names)) found = set([c['search'] for c in commodities] + [m['search'] for m in modules] + [s['search'] for s in ships]) if len(found) < len(names): missing = [name for name in names if name not in found] - yield from bot.say(message.channel, '{}?'.format(', '.join(missing))) - return None + return (None, '{}?'.format(', '.join(missing))) params = {} if len(commodities): params['sellsCommodityIds'] = ','.join([str(c['id']) for c in commodities]) @@ -257,7 +261,7 @@ class Shopping(object): params['sellingModulesIdsString'] = ','.join([str(m['id']) for m in modules]) if len(ships): params['sellingShipIdsString'] = ','.join([str(s['id']) for s in ships]) - return params + return (params, None) def format_station(self, station, params, reference_coords = None): text = '' @@ -291,12 +295,11 @@ class Shopping(object): text += ' updated {}'.format(bot.ago(updated)) return text - @asyncio.coroutine def find_shopping_stations(self, message, names, *, discount = False, large = False, orbital = False, system = None): log.info('Shopping for {}; discount={}; system={}.'.format(', '.join(names), discount, system)) - params = yield from self.build_shopping_list(message, names) + params, complaint = self.build_shopping_list(names) if params is None: - return False + return (False, [complaint]) reference_coords = None @@ -305,12 +308,10 @@ class Shopping(object): if len(systems) > 1: if systems[0]['name'].lower() != system.lower(): log.warning('More than one system matched {}.'.format(system)) - yield from bot.say(message.channel, 'Which system? {}', ','.join([s['name'] for s in systems])) - return False + return (False, ['Which system? {}', ','.join([s['name'] for s in systems])]) elif not len(systems): log.warning('No system matched {}.'.format(system)) - yield from bot.say(message.channel, 'Where is {}?'.format(system)) - return False + return (False, ['Where is {}?'.format(system)]) params['referenceSystemId'] = systems[0]['id'] if 'x' in systems[0]: reference_coords = { 'x': systems[0]['x'], 'y': systems[0]['y'], 'z': systems[0]['z'] } @@ -335,6 +336,6 @@ class Shopping(object): lines.append(self.format_station(station, params, reference_coords)) if not len(lines): lines = ['*shrugs*'] - if not message.channel.is_private: + if not bot.is_private(message.channel): lines.insert(0, message.author.mention) - yield from bot.say_many(message.channel, lines) + return (True, lines) diff --git a/plugin/typing/typing.py b/plugins/typing/__init__.py similarity index 90% rename from plugin/typing/typing.py rename to plugins/typing/__init__.py index 77abda4..7c95171 100644 --- a/plugin/typing/typing.py +++ b/plugins/typing/__init__.py @@ -19,7 +19,7 @@ class Typing(object): return 'User is typing...' def key(self, user, channel): - return '@'.join([user.id, channel.id]) + return '{}@{}'.format(user.id, channel.id) def delete(self, key): try: @@ -59,7 +59,7 @@ class Typing(object): times = self.times(key) times.append(when) if len(times) > self.threshold and max(times) - min(times) >= self.min_ttl: - await bot.maybe_say(channel, '**{}** is typing ...'.format(user.nick if user.nick is not None else user.name), probability = self.probability) + await bot.maybe_say(channel, '**{}** is typing...'.format(user.nick if user.nick is not None else user.name), probability = self.probability) self.cleanup() else: log.debug('Remembering {}'.format(key)) diff --git a/requirements.txt b/requirements.txt index b539b3c..b6b963d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bs4 -discord.py-unofficial[voice]==0.17.1 +discord.py[voice]==1.5.1 feedparser matplotlib pexpect