255 lines
9.8 KiB
Python
Executable file
255 lines
9.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import asyncio
|
|
import discord
|
|
import json
|
|
import random
|
|
import re
|
|
import sys
|
|
|
|
BLOCKS_FILE = 'blocks.json'
|
|
|
|
def get_blocks():
|
|
try:
|
|
with open(BLOCKS_FILE) as f:
|
|
return {int(k): v for (k, v) in json.load(f).items()}
|
|
except FileNotFoundError:
|
|
return {}
|
|
|
|
def add_block(requester, target):
|
|
blocks = get_blocks()
|
|
if requester not in blocks:
|
|
blocks[requester] = []
|
|
if target not in blocks[requester]:
|
|
blocks[requester].append(target)
|
|
with open(BLOCKS_FILE, 'w') as f:
|
|
json.dump(blocks, f)
|
|
|
|
def remove_block(requester, target):
|
|
blocks = get_blocks()
|
|
if requester not in blocks:
|
|
return
|
|
while target in blocks[requester]:
|
|
blocks[requester].remove(target)
|
|
if len(blocks[requester]) == 0:
|
|
del blocks[requester]
|
|
with open(BLOCKS_FILE, 'w') as f:
|
|
json.dump(blocks, f)
|
|
|
|
def matchings(l):
|
|
count = len(l)
|
|
if count == 0:
|
|
return []
|
|
l = l.copy()
|
|
random.shuffle(l)
|
|
ids = set(u.id for u in l)
|
|
|
|
blocks = get_blocks()
|
|
block_pairs = set()
|
|
for r in blocks:
|
|
if r in ids:
|
|
for t in blocks[r]:
|
|
if t in ids:
|
|
block_pairs.add((min(r,t), max(r,t)))
|
|
disjoint_pairs = []
|
|
while True:
|
|
paired = set(u for pair in disjoint_pairs for u in pair)
|
|
remaining = set(p for p in block_pairs if p[0] not in paired and p[1] not in paired)
|
|
if len(remaining) == 0:
|
|
break
|
|
disjoint_pairs.append(remaining.pop())
|
|
|
|
circle = [None]*(count - 1)
|
|
center = None
|
|
for i in range(count // 2):
|
|
if len(disjoint_pairs) != 0:
|
|
p = disjoint_pairs.pop()
|
|
[u0, u1] = [u for u in l if u.id in p]
|
|
l = [u for u in l if u.id not in p]
|
|
else:
|
|
u0 = l.pop()
|
|
u1 = l.pop()
|
|
|
|
circle[i] = u0
|
|
if i != count // 2 - 1:
|
|
circle[-(i+1)] = u1
|
|
else:
|
|
center = u1
|
|
|
|
result = []
|
|
for i in range(len(circle)):
|
|
matching = [(center, circle[i])]
|
|
for j in range(1, (len(circle)-1)//2 + 1):
|
|
matching.append((circle[(i-j)%len(circle)], circle[(i+j)%len(circle)]))
|
|
result.append(matching)
|
|
|
|
random.shuffle(result)
|
|
for i in range(len(result)):
|
|
random.shuffle(result[i])
|
|
for j in range(len(result[i])):
|
|
if random.randrange(2) == 0:
|
|
result[i][j] = (result[i][j][1], result[i][j][0])
|
|
result = [matching for matching in result if all((min(match[0].id,match[1].id), max(match[0].id,match[1].id)) not in block_pairs for match in matching)]
|
|
return result
|
|
|
|
async def delete_if_possible(channels):
|
|
for c in channels:
|
|
try:
|
|
await c.delete()
|
|
except:
|
|
pass
|
|
|
|
async def countdown(channel):
|
|
await asyncio.sleep(20)
|
|
await channel.send('10', tts=True)
|
|
await asyncio.sleep(5)
|
|
await channel.send('5', tts=True)
|
|
await asyncio.sleep(5)
|
|
|
|
with open('config.json') as f:
|
|
config = json.load(f)
|
|
|
|
client = discord.Client()
|
|
|
|
async def handle_guild_message(message):
|
|
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 6` for at most 6 rounds of pollination, `!waggle` for as many rounds as possible until everyone's met everyone else")
|
|
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))
|
|
|
|
mention_all = ' '.join(u.mention for u in participants)
|
|
|
|
schedule = matchings(participants)
|
|
|
|
if rounds:
|
|
schedule = schedule[:rounds]
|
|
|
|
if len(schedule) == 0:
|
|
await message.channel.send("there aren't enough people for pollination right now :(")
|
|
return
|
|
|
|
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
|
|
|
|
await message.channel.send("{} Welcome to pollination! You'll be randomly paired up, moved into separate voice channels, spend some time chatting, and then move to a new channel and meet somebody else. Each round will last 5 minutes, and there'll be {} rounds total. I'll announce when your time is nearly up so you can wrap up your conversations, and I'll automatically move you to a different voice channel at the start of each new round. If you keep this text channel open, you'll get audible countdowns via text-to-speech at the end of each round. **Feel free to leave at any time if you need to. You won't get moved into new voice channels if you disconnect from voice.** If you find yourself alone in a channel, the person you were paired with for that round may have left; just relax and take a break for 5 minutes. Have fun!".format(mention_all, len(schedule)))
|
|
|
|
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 countdown(message.channel)
|
|
for ((a, b), c) in zip(matching, pollination_channels):
|
|
try:
|
|
await a.move_to(c)
|
|
except e:
|
|
print('failed to move participant {}: {}'.format(a, e), file=sys.stderr)
|
|
try:
|
|
await b.move_to(c)
|
|
except e:
|
|
print('failed to move participant {}: {}'.format(b, e), file=sys.stderr)
|
|
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 countdown(message.channel)
|
|
for u in participants:
|
|
try:
|
|
await u.move_to(main_voice_channel)
|
|
except e:
|
|
print('failed to move participant {}: {}'.format(u, e), file=sys.stderr)
|
|
|
|
await delete_if_possible(pollination_channels)
|
|
|
|
async def handle_dm(message):
|
|
requester = message.author.id
|
|
match = re.search('@([^#]+)#([0-9a-f]+)', message.content)
|
|
if match:
|
|
handle = match.group(0)
|
|
name = match.group(1)
|
|
discriminator = match.group(2)
|
|
matching_users = [u for u in client.users if u.name == name and u.discriminator == discriminator]
|
|
if len(matching_users) == 0:
|
|
await message.channel.send("sorry, I can't find user {}, it looks like they're not in any of the discords I'm in".format(handle))
|
|
else:
|
|
target = matching_users[0].id
|
|
if target in get_blocks().get(requester, []):
|
|
await message.channel.send("removing {} from your list of blocked users for pollination".format(handle))
|
|
remove_block(requester, target)
|
|
else:
|
|
await message.channel.send("adding {} to your list of blocked users for pollination".format(handle))
|
|
add_block(requester, target)
|
|
|
|
blocks = get_blocks().get(requester, [])
|
|
if len(blocks) == 0:
|
|
await message.channel.send("you currently don't have anyone blocked for pollination")
|
|
else:
|
|
await message.channel.send("your current list of blocked users for pollination is:\n{}".format('\n'.join('@{}#{}'.format(u.name, u.discriminator) for u in [client.get_user(uid) for uid in blocks] if u is not None)))
|
|
await message.channel.send("to block or unblock a user, DM me their handle, like @creep#0000. to see this message and your list of blocked users, DM me something random. buzz buzz!")
|
|
|
|
@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 isinstance(message.channel, discord.TextChannel):
|
|
await handle_guild_message(message)
|
|
elif isinstance(message.channel, discord.DMChannel):
|
|
await handle_dm(message)
|
|
|
|
client.run(config['token'])
|