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.
import asyncio
import datetime
-import discord_unofficial as discord
+import discord
import logging
import multiprocessing
import os
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):
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)
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
elif command == 'delete':
# delete
- if message.channel.is_private:
+ if bot.is_private(message.channel):
await cat.hiss(message.channel, wake = False)
return
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':
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':
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:
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': {
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)
@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']:
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))
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)
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)
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)<user>: 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:
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.')
import asyncio
import datetime
-import discord_unofficial as discord
+import discord
import hashlib
import io
import math
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),
'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']
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()
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():
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
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
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
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:
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
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] == '#':
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
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
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))
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
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))
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
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
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):
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:
# Get my idle status.
def status():
- for server in client.servers:
+ for server in client.guilds:
return server.me.status
return None
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:
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)
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()
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:
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()')
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)
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)
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 }
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:
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))
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):
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)
-import discord_unofficial as discord
+import discord
import logging
import sqlite3
import re
import uuid
from types import GeneratorType
+import bot
+
log = logging.getLogger('db')
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()
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
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))
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)))
# 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))
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))
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
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
import asyncio
+import random
from plugins import PluginCommand
import bot
+import cat
class Affection(object):
def __init__(self):
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:
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)
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'
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:
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
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
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))
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:
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())
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:
elif param == 'here':
if arg != 'in':
break
- if message.channel.is_private:
+ if bot.is_private(message.channel):
parsed[k] = 'private'
ok = True
else:
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:
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)
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.
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'^(<?@\S+\s)+', '', message.content).strip()
class Bling(object):
def __init__(self):
- self.path = os.path.dirname(__file__)[(len(os.getcwd()) + 1):]
+ self.path = os.path.split(__file__)[0]
+ self.prefix = os.path.join(*self.path.split(os.path.sep)[-3:])
self.fgname = 'fg.png'
self.bgname = 'bg.png'
self.maskname = 'mask.png'
'```bling --profiles```',
'List the `PROFILE`s that can be used with `--profile` to generate avatars.'
'',
- 'If no `PROFILE` is specified with `--profile` I will use the `default` profile. All `PROFILE`s, including `default`, are installed by my administrator under a subdirectory of `{0}`. The background image should be installed as `{1}` and the foreground image should be installed as `{2}`, for example `{0}{3}default{3}{1} and {0}{3}default{3}{2}.'.format(self.path, self.bgname, self.fgname, os.sep),
- 'Your image will be overlaid *above* {} and *beneath* {}.'.format(self.bgname, self.fgname)
+ 'If no `PROFILE` is specified with `--profile` I will use the `default` profile. All `PROFILE`s, including `default`, are installed by my administrator under a subdirectory of `{0}`. The background image should be installed as `{1}` and the foreground image should be installed as `{2}`, for example `{0}{3}default{3}{1}` and `{0}{3}default{3}{2}`.'.format(self.prefix, self.bgname, self.fgname, os.sep),
+ 'Your image will be overlaid *above* `{}` and *beneath* `{}`.'.format(self.bgname, self.fgname)
]
await bot.say_many(message.channel, lines)
def parse_message(self, message, name):
parsed = {
'filename': message.author.name,
- 'mentions': [message.author.mention] if not message.channel.is_private else None,
+ 'mentions': [message.author.mention] if not bot.is_private(message.channel) else None,
'url': None
}
if name is not None:
id = bot.parse_mention(name, author = message.author)
- m = re.match(r'^\d+', id) if id is not None else None
+ m = re.match(r'^\d+', str(id)) if id is not None else None
if m is not None:
other = None
for member in client.get_all_members():
await cat.yelp(message.channel)
return
- await client.send_typing(message.channel)
+ with message.channel.typing() as typing:
+ data = self.create_avatar(parsed['url'], os.sep.join([self.path, profile, self.bgname]), os.sep.join([self.path, profile, self.fgname]), os.sep.join([self.path, profile, self.maskname]))
- data = self.create_avatar(parsed['url'], os.sep.join([self.path, profile, self.bgname]), os.sep.join([self.path, profile, self.fgname]), os.sep.join([self.path, profile, self.maskname]))
- if data is None:
- await cat.yelp(message.channel)
- return
-
- text = ' '.join(parsed['mentions']) if parsed['mentions'] is not None else 'purr'
- await bot.say(message.channel, text, attachment = bot.parse_attachment(data, parsed['filename'] + '.png'))
+ if data:
+ text = ' '.join(parsed['mentions']) if parsed['mentions'] is not None else 'purr'
+ await bot.say(message.channel, text, attachment = bot.parse_attachment(data, parsed['filename'] + '.png'), typing = typing)
+ else:
+ await cat.yelp(message.channel, typing = typing)
def create_avatar(self, url, bgfile, fgfile, maskfile = None):
# Get bling images.
import asyncio
-import discord_unofficial as discord
+import discord
import shlex
import re
import sys
from plugins import PluginCommand
from enum import Enum
-from throwingargparse import ThrowingArgumentParser
+from throwingargparse import ThrowingArgumentParser, ArgumentParserError
import bot
import cat
return 'User'
elif type(instance) == discord.role.Role:
return 'Role'
- elif type(instance) == discord.channel.Channel:
- if instance.type == discord.ChannelType.voice:
- return 'Voice Channel ({}bps)'.format(instance.bitrate)
+ elif isinstance(instance, discord.TextChannel):
return 'Channel'
- elif type(instance) == discord.server.Server:
+ elif isinstance(instance, discord.VoiceChannel):
+ return 'Voice Channel ({}bps)'.format(instance.bitrate)
+ elif type(instance) == discord.guild.Guild:
return 'Server'
class Developer(object):
def valid_commands(self):
return ['digest', 'id']
- @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.run_command(message, shlex.split(raw))
+ await self.run_command(message, shlex.split(raw))
return PluginCommand.exclusive
- @asyncio.coroutine
- def handle_help(self, message, command, *args):
- yield from self.help_command(message, command, *args)
+ async def handle_help(self, message, command, *args):
+ await self.help_command(message, command, *args)
- @asyncio.coroutine
- def run_command(self, message, args):
+ async def run_command(self, message, args):
command = args[0]
if command == 'digest':
- yield from self.handle_digest(message, args[1:])
+ await self.handle_digest(message, args[1:])
elif command == 'id':
- yield from self.handle_id(message, args[1:])
+ await self.handle_id(message, args[1:])
- @asyncio.coroutine
- def help_command(self, message, command):
+ async def help_command(self, message, command):
if command == 'digest':
lines = [
'Post the digest of a message.',
]
else:
lines = ['*shrugs*']
- yield from bot.say_many(message.channel, lines)
+ await bot.say_many(message.channel, lines)
- @asyncio.coroutine
- def get_message(self, message, channel_id, message_id):
+ async def get_message(self, message, channel_id, message_id):
channel = bot.parse_channel(channel_id, author = message.author, voice_ok = False)
if not channel:
- yield from bot.say(message.channel, 'Channel?')
+ await bot.say(message.channel, 'Channel?')
return None
- member = channel.server.get_member(message.author.id)
+ member = channel.guild.get_member(message.author.id)
if member is None:
- log.info('User {} is not a member of channel {} server {}.'.format(message.author, channel_id, channel.server))
+ log.info('User {} is not a member of channel {} server {}.'.format(message.author, channel_id, channel.guild))
permissions = channel.permissions_for(member)
if permissions is None:
log.info('User {} has no permissions in channel {}.'.format(message.author, channel_id))
- yield from bot.say(message.channel, 'Channel?')
+ await bot.say(message.channel, 'Channel?')
return None
if not permissions.read_message_history:
log.info('User {} has no read message history permission in channel {}.'.format(message.author, channel_id))
- yield from bot.say(message.channel, 'Message?')
+ await bot.say(message.channel, 'Message?')
return None
m = re.match(r'^\d+$', message_id)
if m is not None:
try:
- result = yield from client.get_message(channel, message_id)
+ result = await channel.fetch_message(message_id)
if result is not None:
return result
except discord.errors.Forbidden:
log.warning('Forbidden to retrieve message {} in {}.'.format(message_id, channel_id))
- yield from bot.say(message.channel, 'Message?')
+ await bot.say(message.channel, 'Message?')
return None
- @asyncio.coroutine
- def handle_digest(self, message, args):
+ async def handle_digest(self, message, args):
if len(args) > 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
try:
parsed = ap.parse_args(args)
except ArgumentParserError:
- yield from cat.yelp(message.channel)
+ await cat.yelp(message.channel)
return
if parsed.target:
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:
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:
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
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))])
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
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):
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):
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):
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
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):
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()
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)
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)
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))
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:
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
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
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))
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'
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
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
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)
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:
import asyncio
import bs4
-import discord_unofficial as discord
+import discord
import feedparser
import json
import math
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):
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
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:
# 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:
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)
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)
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:
import asyncio
-import discord_unofficial as discord
+import discord
import re
import shlex
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
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
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):
# 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:
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))
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'])
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:
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:
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.
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
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
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:
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),
'',
'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 = {}
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:
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))
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)
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
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?')
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)
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:',
'',
'',
'`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
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]
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:
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
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)
import asyncio
-import discord_unofficial as discord
+import discord
import shlex
from db import DBConnection
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'):
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]
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)
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))
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
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:
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:
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
}
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:
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:
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):
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
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):
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)
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'])
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]```',
'',
'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:
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])
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 = ''
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
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'] }
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)
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:
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))
bs4
-discord.py-unofficial[voice]==0.17.1
+discord.py[voice]==1.5.1
feedparser
matplotlib
pexpect