From febfbd02e08bc6e2b61712eb388170e35ee4e7c1 Mon Sep 17 00:00:00 2001 From: jweigele Date: Mon, 7 Sep 2020 22:56:26 -0700 Subject: [PATCH] Classy updates and renames to get horns working more flexibly in discord --- Dockerfile | 17 +++ wigglydiscord.py => grahbot.py | 198 ++++++++++++++++++++++++++------- 2 files changed, 173 insertions(+), 42 deletions(-) create mode 100644 Dockerfile rename wigglydiscord.py => grahbot.py (60%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ebfe783 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Use the official image as a parent image. +FROM ubuntu + +# Set the working directory. +WORKDIR /usr/src/app + +# Run the command inside your image filesystem. +#RUN npm install +RUN apt-get update +RUN apt-get install python3 python3-pip ffmpeg --no-install-recommends -y +RUN pip3 install discord.py asynqp PyNaCl + +# Run the specified command within the container. +CMD [ "/usr/src/app/grahbot.py" ] + +# Copy the rest of your app's source code from your host to your image filesystem. +COPY grahbot.py . diff --git a/wigglydiscord.py b/grahbot.py similarity index 60% rename from wigglydiscord.py rename to grahbot.py index ca8977b..e02188c 100755 --- a/wigglydiscord.py +++ b/grahbot.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -import sys import discord import socket import random @@ -12,7 +11,76 @@ import asyncio import json import traceback import os +from collections import defaultdict, OrderedDict +PERSISTENT_DIR = '/var/lib/grahbot' + + +class GrahState(object): + FILENAME = '{}/state.json'.format(PERSISTENT_DIR) + def __init__(self, grahbot, filename=FILENAME): + self.filename = filename + self.voice = defaultdict(lambda: None) + self.grahbot = grahbot + self.load() + + def load(self): + if os.path.exists(self.filename): + self.data = json.load(open(self.filename, 'r')) + else: + self.data = OrderedDict() + if 'user' not in self.data: + self.data['user'] = {} + if 'guild' not in self.data: + self.data['guild'] = {} + self.save() + + def save(self): + json.dump(self.data, open(self.filename, 'w')) + + + def set_voice(self, guild, voice): + self.set_voice_by_id(guild.id, voice) + + def set_voice_by_id(self, guild_id, voice): + self.data['guild'][str(guild_id)] = voice.channel.id + if not self.voice[str(guild_id)]: + self.voice[str(guild_id)] = voice + self.save() + + + def get_voice_id(self, guild): + print('here is self.data for guild {}'.format(self.data['guild'])) + if str(guild.id) in self.data['guild']: + return discord.utils.get(self.grahbot.voice_channels, id=self.data['guild'][str(guild.id)]) + else: + return None + + def get_voice(self, guild): + return self.get_voice_by_id(guild.id) + + def get_voice_by_id(self, guild_id): + if str(guild_id) in self.voice: + return self.voice[str(guild_id)] + else: + return None + + def get_user_guild(self, user): + print('Get user guild enter for {}'.format(user.id)) + if str(user.id) in self.data['user']: + print('Found user, returning..') + retval = discord.utils.get(self.grahbot.guilds, id=self.data['user'][str(user.id)]) + print(retval) + if type(retval) != list: + retval = [retval] + return retval + else: + return None + + def set_user_guild(self, user, guild): + self.data['user'][str(user.id)] = guild.id + self.save() + class HornClient(object): def __init__(self, config): self.rabbit_config = config @@ -20,8 +88,8 @@ class HornClient(object): def process_msg(self, msg): print('>> {}'.format(msg.body)) - @asyncio.coroutine async def rabbit_connect(self): + print('Creating rabbitmq socket') # CREATE SOCKET sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -30,16 +98,21 @@ class HornClient(object): sock.connect((self.rabbit_config['host'], int(self.rabbit_config['port']))) connection = await asynqp.connect(virtual_host=self.rabbit_config['vhost'], username=self.rabbit_config['user'], password=self.rabbit_config['password'], sock=sock) + print('Connected to rabbitmq') channel = await connection.open_channel() + print('Declaring exchange') exchange = await channel.declare_exchange(self.rabbit_config['exchange'], 'topic', passive=True) # we have 10 users. Set up a queue for each of them # use different channels to avoid any interference # during message consumption, just in case. self.channel = await connection.open_channel() + print('Declaring queue') self.queue = await self.channel.declare_queue('horn_listener') await self.queue.bind(exchange, routing_key=self.rabbit_config['exchange']) + print('Bound') await self.queue.consume(self.process_msg, no_ack=True) + print('Consumed a thing') # deliver 10 messages to each user while (True): @@ -56,6 +129,7 @@ class GrahDiscordException(Exception): class GrahDiscordBot(discord.Client, HornClient): AUDIO_FILES = ['mp3', '.ogg'] def process_msg(self, msg): + print('Received a message!!! {}'.format(msg)) if msg._properties['content_type'] == 'application/json': # decode the object from json obj = json.loads(msg.body.decode(msg._properties['content_encoding'])) @@ -64,27 +138,27 @@ class GrahDiscordBot(discord.Client, HornClient): if 'output_type' in obj and obj['output_type'] == 'discord': print('Received a horn for us! {}'.format(msg.body)) - asyncio.async(self.horn(dict(sample_name=obj['sample_name']), properties=None)) + asyncio.ensure_future(self.horn(dict(sample_name=obj['sample_name']), properties=None)) else: print('Received a horn not for us: {}'.format(msg.body)) def __init__(self, config): self.config = config - self.voice = {} self.player = None self.used = [] + self.state = GrahState(grahbot=self) self.loop = asyncio.get_event_loop() #self.horn_client = HornClient('config-piege.json') HornClient.__init__(self, config) + self.rabbit = self.loop.create_task(self.rabbit_connect()) discord.Client.__init__(self, loop=self.loop) - asyncio.async(self.rabbit_connect()) self.loop_forever() def terminate_all(self, guild): - if self.voice[guild]: - if self.voice[guild].is_playing(): - self.voice[guild].stop() + if self.state.get_voice(guild): + if self.state.get_voice(guild).is_playing(): + self.state.get_voice(guild).stop() def get_sample_name(self): selections = os.listdir(self.config['airhorn_directory']) @@ -103,7 +177,6 @@ class GrahDiscordBot(discord.Client, HornClient): print('{} selected'.format(sample_name)) return sample_name - @asyncio.coroutine async def horn(self, obj, properties): try: print('Horn! {}'.format(obj)) @@ -124,7 +197,7 @@ class GrahDiscordBot(discord.Client, HornClient): traceback.print_exc() print('Error object was: {}'.format(obj)) else: - await self.msg_play_filename(sample_name, exclusive=exclusive) + await self.msg_play_filename(sample_name, guild_id=336396117791211522, exclusive=exclusive) @@ -135,7 +208,7 @@ class GrahDiscordBot(discord.Client, HornClient): self.loop.run_until_complete(self.start(self.config['token'])) except KeyboardInterrupt: self.loop.run_until_complete(self.logout()) - pending = asyncio.Task.all_tasks(loop=self.loop) + pending = asyncio.all_tasks(loop=self.loop) gathered = asyncio.gather(*pending, loop=self.loop) try: gathered.cancel() @@ -146,22 +219,24 @@ class GrahDiscordBot(discord.Client, HornClient): gathered.exception() except: pass + except Exception: + pass finally: self.loop.close() - @asyncio.coroutine async def on_ready(self): print('Logged in as {} ({})'.format(self.user.name, self.user.id)) print([x for x in self.get_all_channels()]) + #await self.user.edit(nick='Varimathras') print(dir(self)) print(list(self.guilds)) for guild in self.guilds: - self.voice[guild] = None + print('Checking for rejoin of {}'.format(guild.__repr__())) + if self.state.get_voice_id(guild): + print('Rejoining prior voice channel') + await self.msg_join_voice_channel(self.state.get_voice_id(guild).name, guild) + self.member_guilds = {} print(self.voice_clients) -# if len(self.voice_clients) > 0: -# print('Found a preexisting voice client: {}'.format(self.voice_clients)) -# self.voice[guild] = self.voice_clients[0] - #print(self.is_voice_connected(list(self.guilds)[0])) print('------') @property @@ -188,7 +263,6 @@ class GrahDiscordBot(discord.Client, HornClient): else: return False - @asyncio.coroutine async def msg_play_sound(self, name=None, guild=None): if name: filename=None @@ -198,13 +272,13 @@ class GrahDiscordBot(discord.Client, HornClient): break if not filename or not os.path.exists(filename): raise GrahDiscordException("File '{}' not found in airhorn directory".format(name)) - await self.msg_play_filename(filename, guild=guild) + await self.msg_play_filename(filename, guild_id=guild.id) - @asyncio.coroutine - async def msg_play_filename(self, filename, exclusive=False, guild=None): - if self.voice[guild]: + async def msg_play_filename(self, filename, guild_id, exclusive=False): + guild = discord.utils.get(self.guilds, id=guild_id) + if self.state.get_voice(guild): self.terminate_all(guild=guild) - self.voice[guild].play(discord.FFmpegPCMAudio(filename)) + self.state.get_voice(guild).play(discord.FFmpegPCMAudio(filename)) #self.player.start() else: print('Was asked to play {} but no voice channel'.format(filename)) @@ -215,13 +289,13 @@ class GrahDiscordBot(discord.Client, HornClient): for vc in self.voice_channels: if vc.name == channel_name and (not guild or vc.guild == guild): print('Found a voice channel to join {} {}'.format(vc, type(vc))) - if self.voice[guild]: - if vc != self.voice[guild].channel: - await self.voice[guild].move_to(vc) + if self.state.get_voice(guild): + if vc != self.state.get_voice(guild).channel: + self.state.set_voice(guild, await self.state.get_voice(guild).move_to(vc)) else: - self.voice[guild] = await vc.connect() + self.state.set_voice(guild, await vc.connect()) print('Joined voice channel {} {} (id: {})'.format(guild, vc.name, vc.id)) - print('Voice now {}'.format(self.voice[guild])) + print('Voice now {}'.format(self.state.get_voice(guild))) print('Exit join voice') @@ -230,7 +304,9 @@ class GrahDiscordBot(discord.Client, HornClient): for x in os.listdir(self.config['airhorn_directory']): if self.is_audio_file(x): retval.append(x.lower()) - return sorted([x[:-4] for x in retval]) + retval = sorted([x[:-4] for x in retval]) + print('Found airhorn files:\n{}'.format('\n'.join(retval))) + return retval def chunk_it_up(self, filename_list): @@ -242,15 +318,16 @@ class GrahDiscordBot(discord.Client, HornClient): cur_chars += len(yieldval) if index + 1 < len(filename_list): if len(filename_list[index+1]) + cur_chars > char_limit: + print('Yielding {}'.format(yieldval)) yield yieldval yieldval = [] cur_chars = 0 if yieldval: + print('Final yield {}'.format(yieldval)) yield yieldval - @asyncio.coroutine async def oh_no(self, channel, exc): print("Exception: {}\t{}".format(channel, exc)) exc_string = 'Ruh Roh! {}: {}'.format(type(exc), str(exc)) @@ -261,26 +338,55 @@ class GrahDiscordBot(discord.Client, HornClient): print('#{} <{}>: {}'.format(message.channel, message.author, message.content)) - def member_guilds(self, member): + def member_guild_list(self, member): return [x for x in self.guilds if member in x.members] def contextual_guild(self, message): channel = message.channel - print(channel) if isinstance(channel, discord.DMChannel): print('Is a DM with {}'.format(channel.recipient)) - retval = self.member_guilds(channel.recipient) + retval = self.member_guild_list(channel.recipient) print('Member guilds: {}'.format(retval)) return retval else: print('Guild: {}'.format(channel.guild)) return [channel.guild] - @asyncio.coroutine + + def channel_guild(self, message): + if isinstance(message.channel, discord.DMChannel): + return self.state.get_user_guild(message.channel.recipient) + else: + return self.contextual_guild(message) + + def select_guild(self, message): + print('Selectguild {}'.format(message.content)) + guilds = self.contextual_guild(message) + if len(guilds) < 2: + raise Exception('No, this is not needed') + guild_name = message.content.replace('!selectguild ', '') + for guild in guilds: + if guild.name == guild_name: + print('Setting member {} to guild {}'.format(message.channel.recipient, guild.name)) + self.member_guilds[message.channel.recipient] = [guild] + self.state.set_user_guild(message.channel.recipient, guild) + break + + + + def debug_info(self): + return socket.gethostname() + async def on_message(self, message): try: if message.author != self.user: - guilds = self.contextual_guild(message) + print('gettin guilds here') + guilds = self.channel_guild(message) + print(guilds) + if message.content.startswith('!selectguild'): + self.select_guild(message) + return + if len(guilds) == 0: return elif len(guilds) >1: @@ -292,7 +398,7 @@ class GrahDiscordBot(discord.Client, HornClient): if message.content.startswith('!sleep'): await asyncio.sleep(5) await message.channel.send('Done sleeping') - elif message.content.startswith('!joinvoice'): + elif message.content.startswith('!join'): self.print_call(message) print('Was instructed to join {} guild {}'.format(message.content.split(' ')[1], str(guild))) await self.msg_join_voice_channel(message.content.split(' ')[1], guild=guild) @@ -303,15 +409,23 @@ class GrahDiscordBot(discord.Client, HornClient): elif message.content == '!list': self.print_call(message) for airhorn_files in self.chunk_it_up(self.get_airhorn_filenames()): - await message.channel.send('```{}```'.format('\n'.join(airhorn_files))) - elif message.content == '!leavevoice': - await self.voice[guild].disconnect() - self.voice[guild] = None + print('Sending {}'.format(airhorn_files)) + await message.channel.send('```\n{}```'.format('\n'.join(airhorn_files))) + elif message.content == '!leave': + await self.state.get_voice(guild).disconnect() + self.state.set_voice(guild, None) + elif message.content.startswith('!selectguild'): + self.print_call(message) + await self.select_guild(message) elif message.content == '!help': - await message.channel.send('```!joinvoice [channel]\n!play [file]\n!list (for all of them)\n!leavevoice```') + self.print_call(message) + await message.channel.send('```\n!join [channel]\n!play [file]\n!list (for all of them)\n!leave```') + elif message.content == '!whoami': + self.print_call(message) + await message.channel.send('```{}```'.format(self.debug_info())) except Exception as e: await self.oh_no(message.channel, e) if __name__ == '__main__': - config = json.load(open(sys.argv[1], 'r')) + config = json.load(open('{}/config.json'.format(PERSISTENT_DIR), 'r')) bot = GrahDiscordBot(config) -- 2.30.2