diff --git a/.gitignore b/.gitignore index 13d1490..18cee6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +config.json + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/LICENSE b/LICENSE index 204b93d..3c42862 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License Copyright (c) +MIT License Copyright (c) 2020 xenofem Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4e668fb..0521a87 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # waggle-dance -Discord bot for moving people between voice channels for quick one-on-one matchups \ No newline at end of file +Discord bot for moving people between voice channels for quick one-on-one matchups + +## Setup + +```bash +git clone https://git.xeno.science/xenofem/waggle-dance +cd waggle-dance +python3 -m venv env +source env/bin/activate +pip install discord.py +cp config.json.example config.json +vi config.json +python waggle-dance.py +``` + +## Configuration + +- `token`: Discord bot API token +- `channel_names`: list of names for voice channels to create diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..0341314 --- /dev/null +++ b/config.json.example @@ -0,0 +1,4 @@ +{ + "channel_names": ["Azalea", "Buttercup", "Cockscomb", "Daffodil", "Forget Me Not", "Gardenia", "Hyacinth", "Iris", "Lily", "Magnolia", "Narcissus", "Petunia", "Rose", "Tulip"], + "token": "tOk3N" +} diff --git a/waggle-dance.py b/waggle-dance.py new file mode 100755 index 0000000..ce8cf9f --- /dev/null +++ b/waggle-dance.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +import asyncio +import discord +import json +import random +import sys + +def matchings(l): + if len(l) == 0: + return [] + l = l.copy() + random.shuffle(l) + center = l.pop() + result = [] + for i in range(len(l)): + matching = [(center, l[i])] + for j in range(1, (len(l)-1)/2 + 1): + matching.append((l[(i-j)%len(l)], l[(i+j)%len(l)])) + result.append(matching) + return result + +async def delete_if_possible(channels): + for c in channels: + try: + await c.delete() + except: + pass + +with open('config.json') as f: + config = json.load(f) + +client = discord.Client() + +@client.event +async def on_ready(): + print('logged in as {0.user}'.format(client), file=sys.stderr) + +@client.event +async def on_message(message): + if message.author == client.user: + return + + if not message.content.startswith('!waggle'): + return + + if not message.author.guild_permissions.administrator: + await message.channel.send('only administrators can start pollination!') + return + + argv = message.content.split() + if len(argv) > 1: + try: + rounds = int(message.content.split()[1]) + except ValueError: + await message.channel.send('usage: `!waggle 5` for 5 rounds of pollination, `!waggle` for as many rounds as possible') + return + else: + rounds = None + + voice_state = message.author.voice + if voice_state is None or voice_state.channel is None: + await message.channel.send('you need to be in the main gathering voice channel to start pollination') + return + main_voice_channel = voice_state.channel + + category = message.channel.category + if category is None: + await message.channel.send("please re-send this message in a channel in the category where you'd like me to create voice channels") + return + + participants = main_voice_channel.members.copy() + if len(participants) % 2 != 0: + participants.remove(message.author) + await message.channel.send('leaving out {} so we have an even number of participants'.format(message.author.mention)) + + if len(participants) == 0: + await message.channel.send('there are no participants! :(') + return + + mention_all = ' '.join(u.mention for u in participants) + + schedule = matchings(participants) + + if rounds: + schedule = schedule[:rounds] + + await message.channel.send('performing {} rounds of pollination'.format(len(schedule))) + + pollination_channels = [] + channel_names = config['channel_names'].copy() + exception_count = 0 + while len(pollination_channels) < len(participants)/2: + if len(channel_names) > 0: + name = channel_names.pop(0) + else: + name = 'pollination-{}'.format(random.randrange(10000)) + try: + pollination_channels.append(await category.create_voice_channel(name)) + exception_count = 0 + except discord.Forbidden: + await message.channel.send("looks like I'm not allowed to create voice channels :(") + await delete_if_possible(pollination_channels) + return + except discord.HTTPException as e: + exception_count += 1 + if exception_count > 20: + await message.channel.send("I'm trying to create voice channels, but something's wrong: {}".format(e)) + await delete_if_possible(pollination_channels) + return + continue + + for matching in schedule: + announcement = 'next round starting in 30 seconds:\n' + announcement += '\n'.join('{0} and {1} in {2}'.format(a.mention, b.mention, c.name) for ((a, b), c) in zip(matching, pollination_channels)) + await message.channel.send(announcement) + await asyncio.sleep(30) + for ((a, b), c) in zip(matching, pollination_channels): + try: + await a.move_to(c) + except e: + await message.channel.send('failed to move participant: {}'.format(e)) + try: + await b.move_to(c) + except e: + await message.channel.send('failed to move participant: {}'.format(e)) + await asyncio.sleep(60*3) + await message.channel.send('round ending in 2 minutes {}'.format(mention_all)) + await asyncio.sleep(60) + await message.channel.send('round ending in 1 minute {}'.format(mention_all)) + await asyncio.sleep(30) + + await message.channel.send('returning to main channel in 30 seconds {}'.format(mention_all)) + await asyncio.sleep(30) + for u in participants: + try: + await u.move_to(main_voice_channel) + except e: + await message.channel.send('failed to move participant: {}'.format(e)) + + await delete_if_possible(pollination_channels) + +client.run(config['token'])