From: jweigele Date: Tue, 8 Sep 2020 05:56:26 +0000 (-0700) Subject: Classy updates and renames to get horns working more flexibly in discord X-Git-Url: http://git.hexthepla.net/?a=commitdiff_plain;h=febfbd02e08bc6e2b61712eb388170e35ee4e7c1;p=grahbot Classy updates and renames to get horns working more flexibly in discord --- 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/grahbot.py b/grahbot.py new file mode 100755 index 0000000..e02188c --- /dev/null +++ b/grahbot.py @@ -0,0 +1,431 @@ +#!/usr/bin/python3 + +import discord +import socket +import random +import ssl +import asynqp +import discord.voice_client +import datetime +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 + + def process_msg(self, msg): + print('>> {}'.format(msg.body)) + + async def rabbit_connect(self): + print('Creating rabbitmq socket') + # CREATE SOCKET + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # WRAP SOCKET + sock = ssl.wrap_socket(sock) + 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): + await asyncio.sleep(1) + #msg = asynqp.Message("omg here is the time {}".format(datetime.datetime.now())) + #exchange.publish(msg, routing_key=self.rabbit_config['exchange']) + + +class GrahDiscordException(Exception): + pass + + + +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'])) + else: + obj = msg.body + + if 'output_type' in obj and obj['output_type'] == 'discord': + print('Received a horn for us! {}'.format(msg.body)) + 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.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) + self.loop_forever() + + def terminate_all(self, guild): + 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']) + selections = [x for x in selections if self.is_audio_file(x)] + sample_name = random.choice(selections) + for index, use in enumerate(self.used): + if use not in selections: + self.used.pop(use) + while len(self.used) < len(selections) and sample_name in self.used: + print('already used, researching') + sample_name = random.choice(selections) + self.used.append(sample_name) + if len(self.used) == len(selections): + self.used = [] + sample_name = "{}/{}".format(self.config['airhorn_directory'], sample_name) + print('{} selected'.format(sample_name)) + return sample_name + + async def horn(self, obj, properties): + try: + print('Horn! {}'.format(obj)) + # exclusive access on each horn or not? + if 'exclusive' in obj: + exclusive = obj['exclusive'] + else: + exclusive = False + + if obj['sample_name'] == 'random': + sample_name = self.get_sample_name() + elif obj['sample_name'] == 'terminate': + self.terminate_all() + return None + else: + sample_name = '{}/{}'.format(self.config['airhorn_directory'], obj['sample_name']) + except: + traceback.print_exc() + print('Error object was: {}'.format(obj)) + else: + await self.msg_play_filename(sample_name, guild_id=336396117791211522, exclusive=exclusive) + + + + + # for further manipulation if we want to do funky stuff + def loop_forever(self): + try: + self.loop.run_until_complete(self.start(self.config['token'])) + except KeyboardInterrupt: + self.loop.run_until_complete(self.logout()) + pending = asyncio.all_tasks(loop=self.loop) + gathered = asyncio.gather(*pending, loop=self.loop) + try: + gathered.cancel() + self.loop.run_until_complete(gathered) + + # we want to retrieve any exceptions to make sure that + # they don't nag us about it being un-retrieved. + gathered.exception() + except: + pass + except Exception: + pass + finally: + self.loop.close() + + 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: + 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) + print('------') + + @property + def voice_channels(self): + return [x for x in self.get_all_channels() if x.type == discord.ChannelType.voice] + + + @property + def text_channels(self): + return [x for x in self.get_all_channels() if x.type == discord.ChannelType.text] + + def is_audio_file(self, filename): + fn = filename.lower() + if any([fn.endswith(y) for y in self.AUDIO_FILES]): + return True + else: + return False + + def name_matches_filename(self, name, filename): + name = name.lower() + filename = filename.lower() + if '.'.join(filename.split('.')[:-1]) == name and self.is_audio_file(filename): + return True + else: + return False + + async def msg_play_sound(self, name=None, guild=None): + if name: + filename=None + for fn in os.listdir(self.config['airhorn_directory']): + if self.name_matches_filename(name, fn): + filename = '{}/{}'.format(self.config['airhorn_directory'], fn) + 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_id=guild.id) + + 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.state.get_voice(guild).play(discord.FFmpegPCMAudio(filename)) + #self.player.start() + else: + print('Was asked to play {} but no voice channel'.format(filename)) + + + async def msg_join_voice_channel(self, channel_name, guild): + print('Called join voice channel') + 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.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.state.set_voice(guild, await vc.connect()) + print('Joined voice channel {} {} (id: {})'.format(guild, vc.name, vc.id)) + print('Voice now {}'.format(self.state.get_voice(guild))) + + print('Exit join voice') + + def get_airhorn_filenames(self): + retval = [] + for x in os.listdir(self.config['airhorn_directory']): + if self.is_audio_file(x): + retval.append(x.lower()) + 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): + yieldval = [] + char_limit = 1950 + cur_chars = 0 + for index, filename in enumerate(filename_list): + yieldval.append(filename) + 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 + + + async def oh_no(self, channel, exc): + print("Exception: {}\t{}".format(channel, exc)) + exc_string = 'Ruh Roh! {}: {}'.format(type(exc), str(exc)) + traceback.print_exc() + await channel.send(exc_string) + + def print_call(self, message): + print('#{} <{}>: {}'.format(message.channel, message.author, message.content)) + + + 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 + if isinstance(channel, discord.DMChannel): + print('Is a DM with {}'.format(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] + + + 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: + 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: + await message.channel.send('Too many guilds!!!\n{}'.format('\n'.join([str(x) for x in guilds]))) + return + elif len(guilds) == 1: + guild = guilds[0] + + if message.content.startswith('!sleep'): + await asyncio.sleep(5) + await message.channel.send('Done sleeping') + 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) + elif message.content.startswith('!play'): + self.print_call(message) + print('Was instructed to play {}'.format(message.content.split(' ')[1])) + await self.msg_play_sound(name=' '.join(message.content.split(' ')[1:]), guild=guild) + elif message.content == '!list': + self.print_call(message) + for airhorn_files in self.chunk_it_up(self.get_airhorn_filenames()): + 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': + 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('{}/config.json'.format(PERSISTENT_DIR), 'r')) + bot = GrahDiscordBot(config) diff --git a/wigglydiscord.py b/wigglydiscord.py deleted file mode 100755 index ca8977b..0000000 --- a/wigglydiscord.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/python3 - -import sys -import discord -import socket -import random -import ssl -import asynqp -import discord.voice_client -import datetime -import asyncio -import json -import traceback -import os - -class HornClient(object): - def __init__(self, config): - self.rabbit_config = config - - def process_msg(self, msg): - print('>> {}'.format(msg.body)) - - @asyncio.coroutine - async def rabbit_connect(self): - # CREATE SOCKET - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # WRAP SOCKET - sock = ssl.wrap_socket(sock) - 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) - channel = await connection.open_channel() - 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() - self.queue = await self.channel.declare_queue('horn_listener') - await self.queue.bind(exchange, routing_key=self.rabbit_config['exchange']) - await self.queue.consume(self.process_msg, no_ack=True) - - # deliver 10 messages to each user - while (True): - await asyncio.sleep(1) - #msg = asynqp.Message("omg here is the time {}".format(datetime.datetime.now())) - #exchange.publish(msg, routing_key=self.rabbit_config['exchange']) - - -class GrahDiscordException(Exception): - pass - - - -class GrahDiscordBot(discord.Client, HornClient): - AUDIO_FILES = ['mp3', '.ogg'] - def process_msg(self, msg): - if msg._properties['content_type'] == 'application/json': - # decode the object from json - obj = json.loads(msg.body.decode(msg._properties['content_encoding'])) - else: - obj = msg.body - - 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)) - 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.loop = asyncio.get_event_loop() - #self.horn_client = HornClient('config-piege.json') - HornClient.__init__(self, config) - 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() - - def get_sample_name(self): - selections = os.listdir(self.config['airhorn_directory']) - selections = [x for x in selections if self.is_audio_file(x)] - sample_name = random.choice(selections) - for index, use in enumerate(self.used): - if use not in selections: - self.used.pop(use) - while len(self.used) < len(selections) and sample_name in self.used: - print('already used, researching') - sample_name = random.choice(selections) - self.used.append(sample_name) - if len(self.used) == len(selections): - self.used = [] - sample_name = "{}/{}".format(self.config['airhorn_directory'], sample_name) - print('{} selected'.format(sample_name)) - return sample_name - - @asyncio.coroutine - async def horn(self, obj, properties): - try: - print('Horn! {}'.format(obj)) - # exclusive access on each horn or not? - if 'exclusive' in obj: - exclusive = obj['exclusive'] - else: - exclusive = False - - if obj['sample_name'] == 'random': - sample_name = self.get_sample_name() - elif obj['sample_name'] == 'terminate': - self.terminate_all() - return None - else: - sample_name = '{}/{}'.format(self.config['airhorn_directory'], obj['sample_name']) - except: - traceback.print_exc() - print('Error object was: {}'.format(obj)) - else: - await self.msg_play_filename(sample_name, exclusive=exclusive) - - - - - # for further manipulation if we want to do funky stuff - def loop_forever(self): - try: - 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) - gathered = asyncio.gather(*pending, loop=self.loop) - try: - gathered.cancel() - self.loop.run_until_complete(gathered) - - # we want to retrieve any exceptions to make sure that - # they don't nag us about it being un-retrieved. - gathered.exception() - except: - 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()]) - print(dir(self)) - print(list(self.guilds)) - for guild in self.guilds: - self.voice[guild] = None - 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 - def voice_channels(self): - return [x for x in self.get_all_channels() if x.type == discord.ChannelType.voice] - - - @property - def text_channels(self): - return [x for x in self.get_all_channels() if x.type == discord.ChannelType.text] - - def is_audio_file(self, filename): - fn = filename.lower() - if any([fn.endswith(y) for y in self.AUDIO_FILES]): - return True - else: - return False - - def name_matches_filename(self, name, filename): - name = name.lower() - filename = filename.lower() - if '.'.join(filename.split('.')[:-1]) == name and self.is_audio_file(filename): - return True - else: - return False - - @asyncio.coroutine - async def msg_play_sound(self, name=None, guild=None): - if name: - filename=None - for fn in os.listdir(self.config['airhorn_directory']): - if self.name_matches_filename(name, fn): - filename = '{}/{}'.format(self.config['airhorn_directory'], fn) - 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) - - @asyncio.coroutine - async def msg_play_filename(self, filename, exclusive=False, guild=None): - if self.voice[guild]: - self.terminate_all(guild=guild) - self.voice[guild].play(discord.FFmpegPCMAudio(filename)) - #self.player.start() - else: - print('Was asked to play {} but no voice channel'.format(filename)) - - - async def msg_join_voice_channel(self, channel_name, guild): - print('Called join voice channel') - 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) - else: - self.voice[guild] = await vc.connect() - print('Joined voice channel {} {} (id: {})'.format(guild, vc.name, vc.id)) - print('Voice now {}'.format(self.voice[guild])) - - print('Exit join voice') - - def get_airhorn_filenames(self): - retval = [] - 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]) - - - def chunk_it_up(self, filename_list): - yieldval = [] - char_limit = 1950 - cur_chars = 0 - for index, filename in enumerate(filename_list): - yieldval.append(filename) - cur_chars += len(yieldval) - if index + 1 < len(filename_list): - if len(filename_list[index+1]) + cur_chars > char_limit: - yield yieldval - yieldval = [] - cur_chars = 0 - - if 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)) - traceback.print_exc() - await channel.send(exc_string) - - def print_call(self, message): - print('#{} <{}>: {}'.format(message.channel, message.author, message.content)) - - - def member_guilds(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) - print('Member guilds: {}'.format(retval)) - return retval - else: - print('Guild: {}'.format(channel.guild)) - return [channel.guild] - - @asyncio.coroutine - async def on_message(self, message): - try: - if message.author != self.user: - guilds = self.contextual_guild(message) - if len(guilds) == 0: - return - elif len(guilds) >1: - await message.channel.send('Too many guilds!!!\n{}'.format('\n'.join([str(x) for x in guilds]))) - return - elif len(guilds) == 1: - guild = guilds[0] - - if message.content.startswith('!sleep'): - await asyncio.sleep(5) - await message.channel.send('Done sleeping') - elif message.content.startswith('!joinvoice'): - 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) - elif message.content.startswith('!play'): - self.print_call(message) - print('Was instructed to play {}'.format(message.content.split(' ')[1])) - await self.msg_play_sound(name=' '.join(message.content.split(' ')[1:]), guild=guild) - 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 - elif message.content == '!help': - await message.channel.send('```!joinvoice [channel]\n!play [file]\n!list (for all of them)\n!leavevoice```') - except Exception as e: - await self.oh_no(message.channel, e) - -if __name__ == '__main__': - config = json.load(open(sys.argv[1], 'r')) - bot = GrahDiscordBot(config)