Merge pull request #16 from matnad/v2.1.5

Version 2.2
This commit is contained in:
Matthias Nadler 2019-03-23 11:48:53 +01:00 committed by GitHub
commit 4fc681d4ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 258 additions and 163 deletions

View File

@ -1,3 +1,18 @@
# Changelog for Version 2.2
## New features
- Polls will now automatically activate or close and post themselves to the specified channel
- Improved ❔ functionality: Now lists the current votes for each options
- pm!cmd feature is enabled again with more error logging
- Title and options now support most UTF-8 characters, meaning you can put emojis and special characters in your poll
## Changes and Fixes
- Improved performance and scalability. Should feel a lot more responsive now
- Fixed formatting issues for closed polls
- Export now shows server specific nickname is applicable
- Users can no longer create polls in channels where they don't have "send message" permissions
# Changelog for Version 2.1 # Changelog for Version 2.1
## New features ## New features

View File

@ -1,3 +1,4 @@
import asyncio
import codecs import codecs
import datetime import datetime
import logging import logging
@ -8,8 +9,10 @@ from string import ascii_lowercase, printable
import dateparser import dateparser
import pytz import pytz
import regex
from matplotlib import rcParams from matplotlib import rcParams
from matplotlib.afm import AFM from matplotlib.afm import AFM
from pytz import UnknownTimeZoneError
from unidecode import unidecode from unidecode import unidecode
import discord import discord
@ -135,7 +138,7 @@ class Poll:
# sanitize input # sanitize input
if string is None: if string is None:
raise InvalidInput raise InvalidInput
string = re.sub("[^{}]+".format(printable), "", string) string = regex.sub("\p{C}+", "", string)
if set(string).issubset(set(' ')): if set(string).issubset(set(' ')):
raise InvalidInput raise InvalidInput
return string return string
@ -738,8 +741,8 @@ class Poll:
'duration': self.duration, 'duration': self.duration,
'duration_tz': self.duration_tz, 'duration_tz': self.duration_tz,
'time_created': self.time_created, 'time_created': self.time_created,
'open': await self.is_open(update_db=False), 'open': self.open,
'active': await self.is_active(update_db=False), 'active': self.active,
'activation': self.activation, 'activation': self.activation,
'activation_tz': self.activation_tz, 'activation_tz': self.activation_tz,
'votes': self.votes 'votes': self.votes
@ -800,7 +803,10 @@ class Poll:
member = self.server.get_member(user_id) member = self.server.get_member(user_id)
if self.votes[user_id]['choices'].__len__() == 0: if self.votes[user_id]['choices'].__len__() == 0:
continue continue
export += f'\n{member.name}' name = member.nick
if not name:
name = member.name
export += f'\n{name}'
if self.votes[user_id]['weight'] != 1: if self.votes[user_id]['weight'] != 1:
export += f' (weight: {self.votes[user_id]["weight"]})' export += f' (weight: {self.votes[user_id]["weight"]})'
export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']]) export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']])
@ -814,7 +820,10 @@ class Poll:
member = self.server.get_member(user_id) member = self.server.get_member(user_id)
if self.votes[user_id]['choices'].__len__() == 0: if self.votes[user_id]['choices'].__len__() == 0:
continue continue
export += f'\n{member.name}' name = member.nick
if not name:
name = member.name
export += f'\n{name}'
if self.votes[user_id]['weight'] != 1: if self.votes[user_id]['weight'] != 1:
export += f' (weight: {self.votes[user_id]["weight"]})' export += f' (weight: {self.votes[user_id]["weight"]})'
# export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']]) # export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']])
@ -842,10 +851,12 @@ class Poll:
return None return None
async def from_dict(self, d): async def from_dict(self, d):
self.id = d['_id'] self.id = d['_id']
self.server = self.bot.get_server(str(d['server_id'])) self.server = self.bot.get_server(str(d['server_id']))
self.channel = self.bot.get_channel(str(d['channel_id'])) self.channel = self.bot.get_channel(str(d['channel_id']))
self.author = await self.bot.get_user_info(str(d['author'])) # self.author = await self.bot.get_user_info(str(d['author']))
self.author = self.server.get_member(d['author'])
self.name = d['name'] self.name = d['name']
self.short = d['short'] self.short = d['short']
self.anonymous = d['anonymous'] self.anonymous = d['anonymous']
@ -981,7 +992,7 @@ class Poll:
else: else:
text += f'*You have {self.multiple_choice} choices and can change them.*' text += f'*You have {self.multiple_choice} choices and can change them.*'
else: else:
text = f'*Final Results of the Poll *' text = f'*Final Results of the Poll* '
if self.multiple_choice == 0: if self.multiple_choice == 0:
text += '*(Multiple Choice).*' text += '*(Multiple Choice).*'
elif self.multiple_choice == 1: elif self.multiple_choice == 1:
@ -999,7 +1010,6 @@ class Poll:
# embed = await self.add_field_custom(name='**Options**', value=', '.join(self.get_options()), embed=embed) # embed = await self.add_field_custom(name='**Options**', value=', '.join(self.get_options()), embed=embed)
# embed.set_footer(text='bot is in development') # embed.set_footer(text='bot is in development')
return embed return embed
async def post_embed(self, destination=None): async def post_embed(self, destination=None):
@ -1043,9 +1053,15 @@ class Poll:
tz = pytz.timezone('UTC') tz = pytz.timezone('UTC')
else: else:
# choose one valid timezone with the offset # choose one valid timezone with the offset
tz = pytz.timezone(tz[0]) try:
tz = pytz.timezone(tz[0])
except UnknownTimeZoneError:
tz = pytz.UTC
else: else:
tz = pytz.timezone(self.duration_tz) try:
tz = pytz.timezone(self.duration_tz)
except UnknownTimeZoneError:
tz = pytz.UTC
return dt.astimezone(tz) return dt.astimezone(tz)
@ -1144,6 +1160,7 @@ class Poll:
if choice in self.votes[user.id]['choices']: if choice in self.votes[user.id]['choices']:
if self.anonymous: if self.anonymous:
# anonymous multiple choice -> can't unreact so we toggle with react # anonymous multiple choice -> can't unreact so we toggle with react
logger.warning("Unvoting, should not happen for non anon polls.")
await self.unvote(user, option, message, lock) await self.unvote(user, option, message, lock)
# refresh_poll = False # refresh_poll = False
else: else:
@ -1172,21 +1189,23 @@ class Poll:
return return
# commit # commit
if lock._waiters.__len__() == 0: #if lock._waiters.__len__() == 0:
# updating DB, clearing cache and refresh if necessary # updating DB, clearing cache and refresh if necessary
await self.save_to_db() await self.save_to_db()
await self.bot.poll_refresh_q.put_unique_id( # await self.bot.poll_refresh_q.put_unique_id(
{'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock}) # {'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock})
if self.bot.poll_cache.get(str(self.server.id) + self.short): # if self.bot.poll_cache.get(str(self.server.id) + self.short):
del self.bot.poll_cache[str(self.server.id) + self.short] # del self.bot.poll_cache[str(self.server.id) + self.short]
# refresh # refresh
# if refresh_poll: # if refresh_poll:
# edit message if there is a real change # edit message if there is a real change
# await self.bot.edit_message(message, embed=await self.generate_embed()) # await self.bot.edit_message(message, embed=await self.generate_embed())
# self.bot.poll_refresh_q.append(str(self.id)) # self.bot.poll_refresh_q.append(str(self.id))
else: #else:
# cache the poll until the queue is empty # cache the poll until the queue is empty
self.bot.poll_cache[str(self.server.id)+self.short] = self #self.bot.poll_cache[str(self.server.id)+self.short] = self
# await self.bot.edit_message(message, embed=await self.generate_embed())
asyncio.ensure_future(self.bot.edit_message(message, embed=await self.generate_embed()))
# if refresh_poll: # if refresh_poll:
# await self.bot.poll_refresh_q.put_unique_id({'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock}) # await self.bot.poll_refresh_q.put_unique_id({'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock})
@ -1217,17 +1236,19 @@ class Poll:
if choice != 'invalid' and choice in self.votes[user.id]['choices']: if choice != 'invalid' and choice in self.votes[user.id]['choices']:
try: try:
self.votes[user.id]['choices'].remove(choice) self.votes[user.id]['choices'].remove(choice)
if lock._waiters.__len__() == 0: await self.save_to_db()
# updating DB, clearing cache and refreshing message asyncio.ensure_future(self.bot.edit_message(message, embed=await self.generate_embed()))
await self.save_to_db() # if lock._waiters.__len__() == 0:
await self.bot.poll_refresh_q.put_unique_id( # # updating DB, clearing cache and refreshing message
{'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock}) # await self.save_to_db()
if self.bot.poll_cache.get(str(self.server.id) + self.short): # await self.bot.poll_refresh_q.put_unique_id(
del self.bot.poll_cache[str(self.server.id) + self.short] # {'id': self.id, 'msg': message, 'sid': self.server.id, 'label': self.short, 'lock': lock})
# await self.bot.edit_message(message, embed=await self.generate_embed()) # if self.bot.poll_cache.get(str(self.server.id) + self.short):
else: # del self.bot.poll_cache[str(self.server.id) + self.short]
# cache the poll until the queue is empty # # await self.bot.edit_message(message, embed=await self.generate_embed())
self.bot.poll_cache[str(self.server.id) + self.short] = self # else:
# # cache the poll until the queue is empty
# self.bot.poll_cache[str(self.server.id) + self.short] = self
except ValueError: except ValueError:
pass pass

View File

@ -4,7 +4,9 @@ import copy
import datetime import datetime
import json import json
import logging import logging
import re
import shlex import shlex
import string
import traceback import traceback
import discord import discord
@ -20,32 +22,47 @@ from essentials.settings import SETTINGS
from utils.poll_name_generator import generate_word from utils.poll_name_generator import generate_word
from essentials.exceptions import StopWizard from essentials.exceptions import StopWizard
## A-Z Emojis for Discord
AZ_EMOJIS = [(b'\\U0001f1a'.replace(b'a', bytes(hex(224 + (6 + i))[2:], "utf-8"))).decode("unicode-escape") for i in
range(26)]
class PollControls: class PollControls:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.bot.loop.create_task(self.refresh_polls()) self.bot.loop.create_task(self.close_polls())
self.ignore_next_removed_reaction = {} self.ignore_next_removed_reaction = {}
# General Methods # General Methods
async def refresh_polls(self): async def close_polls(self):
"""This function runs every 5 seconds to refresh poll messages when needed""" """This function runs every 60 seconds to schedule prepared polls and close expired polls"""
while True: while True:
try: try:
for i in range(self.bot.poll_refresh_q.qsize()): query = self.bot.db.polls.find({'active': False, 'activation': {"$not": re.compile("0")}})
values = await self.bot.poll_refresh_q.get() if query:
if values.get('lock') and not values.get('lock')._waiters: for pd in [poll async for poll in query]:
p = await Poll.load_from_db(self.bot, str(values.get('sid')), values.get('label')) p = Poll(self.bot, load=True)
if p: await p.from_dict(pd)
await self.bot.edit_message(values.get('msg'), embed=await p.generate_embed()) if p.active:
await self.bot.send_message(p.channel, 'This poll has been scheduled and is active now!')
await p.post_embed(destination=p.channel)
self.bot.poll_refresh_q.task_done() query = self.bot.db.polls.find({'open': True, 'duration': {"$not": re.compile("0")}})
else: if query:
await self.bot.poll_refresh_q.put_unique_id(values) for pd in [poll async for poll in query]:
p = Poll(self.bot, load=True)
await p.from_dict(pd)
if not p.open:
await self.bot.send_message(p.channel, 'This poll has reached the deadline and is closed!')
await p.post_embed(destination=p.channel)
except AttributeError: except AttributeError:
#Database not loaded yet
logger.warning("Attribute Error in close_polls loop")
pass pass
except:
#Never break this loop due to an error
logger.error("Other Error in close_polls loop")
pass
await asyncio.sleep(5) await asyncio.sleep(5)
def get_lock(self, server_id): def get_lock(self, server_id):
@ -92,7 +109,7 @@ class PollControls:
if short is None: if short is None:
pre = await get_server_pre(self.bot, ctx.message.server) pre = await get_server_pre(self.bot, ctx.message.server)
error = f'Please specify the label of a poll after the close command. \n' \ error = f'Please specify the label of a poll after the activate command. \n' \
f'`{pre}activate <poll_label>`' f'`{pre}activate <poll_label>`'
await self.say_error(ctx, error) await self.say_error(ctx, error)
else: else:
@ -129,7 +146,7 @@ class PollControls:
if short is None: if short is None:
pre = await get_server_pre(self.bot, ctx.message.server) pre = await get_server_pre(self.bot, ctx.message.server)
error = f'Please specify the label of a poll after the delete command. \n' \ error = f'Please specify the label of a poll after the delete command. \n' \
f'`{pre}close <poll_label>`' f'`{pre}delete <poll_label>`'
await self.say_error(ctx, error) await self.say_error(ctx, error)
else: else:
p = await Poll.load_from_db(self.bot, str(server.id), short) p = await Poll.load_from_db(self.bot, str(server.id), short)
@ -201,8 +218,8 @@ class PollControls:
if short is None: if short is None:
pre = await get_server_pre(self.bot, ctx.message.server) pre = await get_server_pre(self.bot, ctx.message.server)
error = f'Please specify the label of a poll after the close command. \n' \ error = f'Please specify the label of a poll after the export command. \n' \
f'`{pre}close <poll_label>`' f'`{pre}export <poll_label>`'
await self.say_error(ctx, error) await self.say_error(ctx, error)
else: else:
p = await Poll.load_from_db(self.bot, str(server.id), short) p = await Poll.load_from_db(self.bot, str(server.id), short)
@ -253,7 +270,7 @@ class PollControls:
else: else:
return return
def item_fct(i,item): def item_fct(i, item):
return f':black_small_square: **{item["short"]}**: {item["name"]}' return f':black_small_square: **{item["short"]}**: {item["name"]}'
title = f' Listing {short} polls' title = f' Listing {short} polls'
@ -281,78 +298,81 @@ class PollControls:
@commands.command(pass_context=True) @commands.command(pass_context=True)
async def cmd(self, ctx, *, cmd=None): async def cmd(self, ctx, *, cmd=None):
'''The old, command style way paired with the wizard.''' '''The old, command style way paired with the wizard.'''
await self.say_embed(ctx, say_text='This command is temporarily disabled.') # await self.say_embed(ctx, say_text='This command is temporarily disabled.')
# server = await ask_for_server(self.bot, ctx.message) server = await ask_for_server(self.bot, ctx.message)
# if not server: if not server:
# return return
# pre = await get_server_pre(self.bot, server) pre = await get_server_pre(self.bot, server)
# try:
# # generate the argparser and handle invalid stuff # generate the argparser and handle invalid stuff
# descr = 'Accept poll settings via commandstring. \n\n' \ descr = 'Accept poll settings via commandstring. \n\n' \
# '**Wrap all arguments in quotes like this:** \n' \ '**Wrap all arguments in quotes like this:** \n' \
# f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \ f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \
# 'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \ 'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \
# 'If an argument is invalid, the wizard will step in. ' \ 'If an argument is invalid, the wizard will step in. ' \
# 'If the command string is invalid, you will get this error :)' 'If the command string is invalid, you will get this error :)'
# parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False) parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False)
# parser.add_argument('-question', '-q') parser.add_argument('-question', '-q')
# parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id))) parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id)))
# parser.add_argument('-options', '-o') parser.add_argument('-options', '-o')
# parser.add_argument('-multiple_choice', '-mc', default='1') parser.add_argument('-multiple_choice', '-mc', default='1')
# parser.add_argument('-roles', '-r', default='all') parser.add_argument('-roles', '-r', default='all')
# parser.add_argument('-weights', '-w', default='none') parser.add_argument('-weights', '-w', default='none')
# parser.add_argument('-deadline', '-d', default='0') parser.add_argument('-deadline', '-d', default='0')
# parser.add_argument('-anonymous', '-a', action="store_true") parser.add_argument('-anonymous', '-a', action="store_true")
#
# helpstring = parser.format_help() helpstring = parser.format_help()
# helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ") helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
#
# if cmd and cmd == 'help': if cmd and cmd == 'help':
# await self.say_embed(ctx, say_text=helpstring) await self.say_embed(ctx, say_text=helpstring)
# return return
#
# try: try:
# cmds = shlex.split(cmd) cmds = shlex.split(cmd)
# except ValueError: except ValueError:
# await self.say_error(ctx, error_text=helpstring) await self.say_error(ctx, error_text=helpstring)
# return return
# except: except:
# return return
#
# try: try:
# args, unknown_args = parser.parse_known_args(cmds) args, unknown_args = parser.parse_known_args(cmds)
# except SystemExit: except SystemExit:
# await self.say_error(ctx, error_text=helpstring) await self.say_error(ctx, error_text=helpstring)
# return return
# except: except:
# return return
#
# if unknown_args: if unknown_args:
# error_text = f'**There was an error reading the command line options!**.\n' \ error_text = f'**There was an error reading the command line options!**.\n' \
# f'Most likely this is because you didn\'t surround the arguments with double quotes like this: ' \ f'Most likely this is because you didn\'t surround the arguments with double quotes like this: ' \
# f'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \ f'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \
# f'\n\nHere are the arguments I could not understand:\n' f'\n\nHere are the arguments I could not understand:\n'
# error_text += '`'+'\n'.join(unknown_args)+'`' error_text += '`'+'\n'.join(unknown_args)+'`'
# error_text += f'\n\nHere are the arguments which are ok:\n' error_text += f'\n\nHere are the arguments which are ok:\n'
# error_text += '`' + '\n'.join([f'{k}: {v}' for k, v in vars(args).items()]) + '`' error_text += '`' + '\n'.join([f'{k}: {v}' for k, v in vars(args).items()]) + '`'
#
# await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.') await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.')
# return return
#
# # pass arguments to the wizard # pass arguments to the wizard
# async def route(poll): async def route(poll):
# await poll.set_name(force=args.question) await poll.set_name(force=args.question)
# await poll.set_short(force=args.label) await poll.set_short(force=args.label)
# await poll.set_anonymous(force=f'{"yes" if args.anonymous else "no"}') await poll.set_anonymous(force=f'{"yes" if args.anonymous else "no"}')
# await poll.set_options_reaction(force=args.options) await poll.set_options_reaction(force=args.options)
# await poll.set_multiple_choice(force=args.multiple_choice) await poll.set_multiple_choice(force=args.multiple_choice)
# await poll.set_roles(force=args.roles) await poll.set_roles(force=args.roles)
# await poll.set_weights(force=args.weights) await poll.set_weights(force=args.weights)
# await poll.set_duration(force=args.deadline) await poll.set_duration(force=args.deadline)
#
# poll = await self.wizard(ctx, route, server) poll = await self.wizard(ctx, route, server)
# if poll: if poll:
# await poll.post_embed(destination=poll.channel) await poll.post_embed(destination=poll.channel)
except Exception as error:
logger.error("ERROR IN pm!cmd")
logger.exception(error)
@commands.command(pass_context=True) @commands.command(pass_context=True)
@ -481,21 +501,29 @@ class PollControls:
# check if removed by the bot.. this is a bit hacky but discord doesn't provide the correct info... # check if removed by the bot.. this is a bit hacky but discord doesn't provide the correct info...
message_id = data.get('message_id') message_id = data.get('message_id')
user_id = data.get('user_id') user_id = data.get('user_id')
if self.ignore_next_removed_reaction.get(str(message_id)+str(emoji)) == user_id: if self.ignore_next_removed_reaction.get(str(message_id) + str(emoji)) == user_id:
del self.ignore_next_removed_reaction[str(message_id)+str(emoji)] del self.ignore_next_removed_reaction[str(message_id) + str(emoji)]
return return
# check if we can find a poll label # check if we can find a poll label
channel_id = data.get('channel_id') channel_id = data.get('channel_id')
channel = self.bot.get_channel(channel_id) channel = self.bot.get_channel(channel_id)
user = await self.bot.get_user_info(user_id) # only do this once # user = await self.bot.get_user_info(user_id) # only do this once
server_id = data.get('guild_id')
if server_id:
server = self.bot.get_server(server_id)
user = server.get_member(user_id)
else:
user = await self.bot.get_user_info(user_id) # only do this once, this is rate limited
if not channel: if not channel:
# discord rapidly closes dm channels by desing # discord rapidly closes dm channels by desing
# put private channels back into the bots cache and try again # put private channels back into the bots cache and try again
await self.bot.start_private_message(user) await self.bot.start_private_message(user)
channel = self.bot.get_channel(channel_id) channel = self.bot.get_channel(channel_id)
message = await self.bot.get_message(channel=channel, id=message_id) message = await self.bot.get_message(channel=channel, id=message_id)
label = None label = None
if message and message.embeds: if message and message.embeds:
@ -508,28 +536,22 @@ class PollControls:
if not label: if not label:
return return
# fetch poll
# create message object for the reaction sender, to get correct server # create message object for the reaction sender, to get correct server
user_msg = copy.deepcopy(message) if not server_id:
user_msg.author = user user_msg = copy.deepcopy(message)
server = await ask_for_server(self.bot, user_msg, label) user_msg.author = user
server = await ask_for_server(self.bot, user_msg, label)
server_id = server.id
# this is exclusive # this is exclusive
lock = self.get_lock(server_id)
lock = self.get_lock(server.id)
async with lock: async with lock:
# try to load poll form cache p = await Poll.load_from_db(self.bot, server_id, label)
p = self.bot.poll_cache.get(str(server.id) + label)
if not p:
p = await Poll.load_from_db(self.bot, server.id, label)
if not isinstance(p, Poll): if not isinstance(p, Poll):
return return
if not p.anonymous: if not p.anonymous:
# for anonymous polls we can't unvote because we need to hide reactions # for anonymous polls we can't unvote because we need to hide reactions
member = server.get_member(user_id) await p.unvote(user, emoji, message, lock)
await p.unvote(member, emoji, message, lock)
async def do_on_reaction_add(self, data): async def do_on_reaction_add(self, data):
# dont look at bot's own reactions # dont look at bot's own reactions
@ -576,9 +598,7 @@ class PollControls:
# hopefully it will scale well enough or I need a different solution # hopefully it will scale well enough or I need a different solution
lock = self.get_lock(server.id) lock = self.get_lock(server.id)
async with lock: async with lock:
p = self.bot.poll_cache.get(str(server.id)+label) p = await Poll.load_from_db(self.bot, server.id, label)
if not p:
p = await Poll.load_from_db(self.bot, server.id, label)
if not isinstance(p, Poll): if not isinstance(p, Poll):
return return
@ -624,8 +644,9 @@ class PollControls:
if user.id in p.votes: if user.id in p.votes:
if p.votes[user.id]['choices'].__len__() > 0: if p.votes[user.id]['choices'].__len__() > 0:
choices = ', '.join([p.options_reaction[c] for c in p.votes[user.id]['choices']]) choices = ', '.join([p.options_reaction[c] for c in p.votes[user.id]['choices']])
embed.add_field(name=f'{"Your current votes (can be changed as long as the poll is open):" if is_open else "Your final votes:"}', embed.add_field(
value=choices, inline=False) name=f'{"Your current votes (can be changed as long as the poll is open):" if is_open else "Your final votes:"}',
value=choices, inline=False)
# weight # weight
if vote_rights: if vote_rights:
@ -646,11 +667,44 @@ class PollControls:
elif deadline == 0: elif deadline == 0:
time_left = 'Until manually closed.' time_left = 'Until manually closed.'
else: else:
time_left = str(deadline-datetime.datetime.utcnow().replace(tzinfo=pytz.utc)).split('.', 2)[0] time_left = str(deadline - datetime.datetime.utcnow().replace(tzinfo=pytz.utc)).split('.', 2)[0]
embed.add_field(name='Time left in the poll:', value=time_left, inline=False) embed.add_field(name='Time left in the poll:', value=time_left, inline=False)
await self.bot.send_message(user, embed=embed) await self.bot.send_message(user, embed=embed)
# send current details of who currently voted for what
if not p.anonymous and p.votes.__len__() > 0:
msg = '--------------------------------------------\n' \
'CURRENT VOTES\n' \
'--------------------------------------------\n'
for i, o in enumerate(p.options_reaction):
if not p.options_reaction_default:
msg += AZ_EMOJIS[i] + " "
msg += "**" +o+":**"
c = 0
for user_id in p.votes:
member = server.get_member(user_id)
if not member or i not in p.votes[user_id]['choices']:
continue
c += 1
name = member.nick
if not name:
name = member.name
msg += f'\n{name}'
if p.votes[user_id]['weight'] != 1:
msg += f' (weight: {p.votes[user_id]["weight"]})'
# msg += ': ' + ', '.join([AZ_EMOJIS[c]+" "+p.options_reaction[c] for c in p.votes[user_id]['choices']])
if msg.__len__() > 1500:
await self.bot.send_message(user, msg)
msg = ''
if c == 0:
msg += '\nNo votes for this option yet.'
msg += '\n\n'
if msg.__len__() > 0:
await self.bot.send_message(user, msg)
return return
# Assume: User wants to vote with reaction # Assume: User wants to vote with reaction
@ -664,7 +718,7 @@ class PollControls:
# check if we need to remove reactions (this will trigger on_reaction_remove) # check if we need to remove reactions (this will trigger on_reaction_remove)
if str(channel.type) != 'private' and p.anonymous: if str(channel.type) != 'private' and p.anonymous:
# immediately remove reaction and to be safe, remove all reactions # immediately remove reaction and to be safe, remove all reactions
self.ignore_next_removed_reaction[str(message.id)+str(emoji)] = user_id self.ignore_next_removed_reaction[str(message.id) + str(emoji)] = user_id
await self.bot.remove_reaction(message, emoji, user) await self.bot.remove_reaction(message, emoji, user)
# order here is crucial since we can't determine if a reaction was removed by the bot or user # order here is crucial since we can't determine if a reaction was removed by the bot or user
@ -673,13 +727,13 @@ class PollControls:
# cant do this until we figure out how to see who removed the reaction? # cant do this until we figure out how to see who removed the reaction?
# for now MC 1 is like MC x # for now MC 1 is like MC x
# if str(channel.type) != 'private' and p.multiple_choice == 1: if str(channel.type) != 'private' and p.multiple_choice == 1:
# # remove all other reactions # remove all other reactions
# # if lock._waiters.__len__() == 0: # if lock._waiters.__len__() == 0:
# for r in message.reactions: for r in message.reactions:
# if r.emoji and r.emoji != emoji: if r.emoji and r.emoji != emoji:
# await self.bot.remove_reaction(message, r.emoji, user) await self.bot.remove_reaction(message, r.emoji, user)
# pass pass
def setup(bot): def setup(bot):

View File

@ -100,8 +100,12 @@ async def ask_for_channel(bot, server, message):
if str(message.channel.type) == 'text': if str(message.channel.type) == 'text':
return message.channel return message.channel
# build channel list that the user is allowed to send messages to
user = message.author
member = server.get_member(user.id)
channel_list = [c for c in server.channels if str(c.type) == 'text' and c.permissions_for(member).send_messages]
# if exactly 1 channel, return it # if exactly 1 channel, return it
channel_list = [c for c in server.channels if str(c.type) == 'text']
if channel_list.__len__() == 1: if channel_list.__len__() == 1:
return channel_list[0] return channel_list[0]

View File

@ -56,7 +56,7 @@ async def on_ready():
bot.db = mongo.pollmaster bot.db = mongo.pollmaster
bot.session = aiohttp.ClientSession() bot.session = aiohttp.ClientSession()
print(bot.db) print(bot.db)
await bot.change_presence(game=discord.Game(name=f'pm!help - v2.1 is live!')) await bot.change_presence(game=discord.Game(name=f'pm!help - v2.2'))
# check discord server configs # check discord server configs
try: try:
@ -69,11 +69,12 @@ async def on_ready():
{'$set': {'prefix': 'pm!', 'admin_role': 'polladmin', 'user_role': 'polluser'}}, {'$set': {'prefix': 'pm!', 'admin_role': 'polladmin', 'user_role': 'polluser'}},
upsert=True upsert=True
) )
try: # stopping migration support.
await import_old_database(bot, server) # try:
print(str(server), "updated.") # await import_old_database(bot, server)
except: # print(str(server), "updated.")
print(str(server.id), "failed.") # except:
# print(str(server.id), "failed.")
except: except:
print("Problem verifying servers.") print("Problem verifying servers.")
@ -82,9 +83,8 @@ async def on_ready():
# global locks and caches for performance when voting rapidly # global locks and caches for performance when voting rapidly
bot.locks = {} bot.locks = {}
bot.poll_cache = {} # bot.poll_cache = {}
# bot.poll_refresh_q = {} # bot.poll_refresh_q = UniqueQueue()
bot.poll_refresh_q = UniqueQueue()
print("Servers verified. Bot running.") print("Servers verified. Bot running.")

View File

@ -1,4 +1,4 @@
# Pollmaster V 2.1 # Pollmaster V 2.2
## Overview ## Overview
@ -34,6 +34,7 @@ Here is how Pollmaster looks in action:
| pm!help | Shows an interactive help menu | | pm!help | Shows an interactive help menu |
| pm!new | Starts a new poll with all the settings | | pm!new | Starts a new poll with all the settings |
| pm!quick | Starts a new poll with just a question and options | | pm!quick | Starts a new poll with just a question and options |
| pm!show <label> | Shows poll in a specified channel (can be different from original channel| |
| pm!prefix <new prefix> | Change the prefix for this server | | pm!prefix <new prefix> | Change the prefix for this server |
| pm!userrole <any role> | Set the role that has the rights to use the bot | | pm!userrole <any role> | Set the role that has the rights to use the bot |