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
This commit is contained in:
parent
6189aaf39a
commit
5ae6a31a20
15
changelog.md
15
changelog.md
@ -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
|
||||
|
||||
## New features
|
||||
|
||||
71
cogs/poll.py
71
cogs/poll.py
@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import codecs
|
||||
import datetime
|
||||
import logging
|
||||
@ -8,8 +9,10 @@ from string import ascii_lowercase, printable
|
||||
|
||||
import dateparser
|
||||
import pytz
|
||||
import regex
|
||||
from matplotlib import rcParams
|
||||
from matplotlib.afm import AFM
|
||||
from pytz import UnknownTimeZoneError
|
||||
from unidecode import unidecode
|
||||
|
||||
import discord
|
||||
@ -135,7 +138,7 @@ class Poll:
|
||||
# sanitize input
|
||||
if string is None:
|
||||
raise InvalidInput
|
||||
string = re.sub("[^{}]+".format(printable), "", string)
|
||||
string = regex.sub("\p{C}+", "", string)
|
||||
if set(string).issubset(set(' ')):
|
||||
raise InvalidInput
|
||||
return string
|
||||
@ -738,8 +741,8 @@ class Poll:
|
||||
'duration': self.duration,
|
||||
'duration_tz': self.duration_tz,
|
||||
'time_created': self.time_created,
|
||||
'open': await self.is_open(update_db=False),
|
||||
'active': await self.is_active(update_db=False),
|
||||
'open': self.open,
|
||||
'active': self.active,
|
||||
'activation': self.activation,
|
||||
'activation_tz': self.activation_tz,
|
||||
'votes': self.votes
|
||||
@ -800,7 +803,10 @@ class Poll:
|
||||
member = self.server.get_member(user_id)
|
||||
if self.votes[user_id]['choices'].__len__() == 0:
|
||||
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:
|
||||
export += f' (weight: {self.votes[user_id]["weight"]})'
|
||||
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)
|
||||
if self.votes[user_id]['choices'].__len__() == 0:
|
||||
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:
|
||||
export += f' (weight: {self.votes[user_id]["weight"]})'
|
||||
# export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']])
|
||||
@ -842,10 +851,12 @@ class Poll:
|
||||
return None
|
||||
|
||||
async def from_dict(self, d):
|
||||
|
||||
self.id = d['_id']
|
||||
self.server = self.bot.get_server(str(d['server_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.short = d['short']
|
||||
self.anonymous = d['anonymous']
|
||||
@ -981,7 +992,7 @@ class Poll:
|
||||
else:
|
||||
text += f'*You have {self.multiple_choice} choices and can change them.*'
|
||||
else:
|
||||
text = f'*Final Results of the Poll *'
|
||||
text = f'*Final Results of the Poll* '
|
||||
if self.multiple_choice == 0:
|
||||
text += '*(Multiple Choice).*'
|
||||
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.set_footer(text='bot is in development')
|
||||
|
||||
return embed
|
||||
|
||||
async def post_embed(self, destination=None):
|
||||
@ -1043,9 +1053,15 @@ class Poll:
|
||||
tz = pytz.timezone('UTC')
|
||||
else:
|
||||
# choose one valid timezone with the offset
|
||||
try:
|
||||
tz = pytz.timezone(tz[0])
|
||||
except UnknownTimeZoneError:
|
||||
tz = pytz.UTC
|
||||
else:
|
||||
try:
|
||||
tz = pytz.timezone(self.duration_tz)
|
||||
except UnknownTimeZoneError:
|
||||
tz = pytz.UTC
|
||||
|
||||
return dt.astimezone(tz)
|
||||
|
||||
@ -1144,6 +1160,7 @@ class Poll:
|
||||
if choice in self.votes[user.id]['choices']:
|
||||
if self.anonymous:
|
||||
# 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)
|
||||
# refresh_poll = False
|
||||
else:
|
||||
@ -1172,21 +1189,23 @@ class Poll:
|
||||
return
|
||||
|
||||
# commit
|
||||
if lock._waiters.__len__() == 0:
|
||||
#if lock._waiters.__len__() == 0:
|
||||
# updating DB, clearing cache and refresh if necessary
|
||||
await self.save_to_db()
|
||||
await self.bot.poll_refresh_q.put_unique_id(
|
||||
{'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):
|
||||
del self.bot.poll_cache[str(self.server.id) + self.short]
|
||||
# await self.bot.poll_refresh_q.put_unique_id(
|
||||
# {'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):
|
||||
# del self.bot.poll_cache[str(self.server.id) + self.short]
|
||||
# refresh
|
||||
# if refresh_poll:
|
||||
# edit message if there is a real change
|
||||
# await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
# self.bot.poll_refresh_q.append(str(self.id))
|
||||
else:
|
||||
#else:
|
||||
# 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:
|
||||
# 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']:
|
||||
try:
|
||||
self.votes[user.id]['choices'].remove(choice)
|
||||
if lock._waiters.__len__() == 0:
|
||||
# updating DB, clearing cache and refreshing message
|
||||
await self.save_to_db()
|
||||
await self.bot.poll_refresh_q.put_unique_id(
|
||||
{'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):
|
||||
del self.bot.poll_cache[str(self.server.id) + self.short]
|
||||
# await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
else:
|
||||
# cache the poll until the queue is empty
|
||||
self.bot.poll_cache[str(self.server.id) + self.short] = self
|
||||
asyncio.ensure_future(self.bot.edit_message(message, embed=await self.generate_embed()))
|
||||
# if lock._waiters.__len__() == 0:
|
||||
# # updating DB, clearing cache and refreshing message
|
||||
# await self.save_to_db()
|
||||
# await self.bot.poll_refresh_q.put_unique_id(
|
||||
# {'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):
|
||||
# del self.bot.poll_cache[str(self.server.id) + self.short]
|
||||
# # await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
# else:
|
||||
# # cache the poll until the queue is empty
|
||||
# self.bot.poll_cache[str(self.server.id) + self.short] = self
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
@ -4,7 +4,9 @@ import copy
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
import string
|
||||
import traceback
|
||||
|
||||
import discord
|
||||
@ -20,32 +22,47 @@ from essentials.settings import SETTINGS
|
||||
from utils.poll_name_generator import generate_word
|
||||
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:
|
||||
def __init__(self, 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 = {}
|
||||
|
||||
|
||||
|
||||
# General Methods
|
||||
async def refresh_polls(self):
|
||||
"""This function runs every 5 seconds to refresh poll messages when needed"""
|
||||
async def close_polls(self):
|
||||
"""This function runs every 60 seconds to schedule prepared polls and close expired polls"""
|
||||
while True:
|
||||
try:
|
||||
for i in range(self.bot.poll_refresh_q.qsize()):
|
||||
values = await self.bot.poll_refresh_q.get()
|
||||
if values.get('lock') and not values.get('lock')._waiters:
|
||||
p = await Poll.load_from_db(self.bot, str(values.get('sid')), values.get('label'))
|
||||
if p:
|
||||
await self.bot.edit_message(values.get('msg'), embed=await p.generate_embed())
|
||||
query = self.bot.db.polls.find({'active': False, 'activation': {"$not": re.compile("0")}})
|
||||
if query:
|
||||
for pd in [poll async for poll in query]:
|
||||
p = Poll(self.bot, load=True)
|
||||
await p.from_dict(pd)
|
||||
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()
|
||||
else:
|
||||
await self.bot.poll_refresh_q.put_unique_id(values)
|
||||
query = self.bot.db.polls.find({'open': True, 'duration': {"$not": re.compile("0")}})
|
||||
if query:
|
||||
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:
|
||||
#Database not loaded yet
|
||||
logger.warning("Attribute Error in close_polls loop")
|
||||
pass
|
||||
except:
|
||||
#Never break this loop due to an error
|
||||
logger.error("Other Error in close_polls loop")
|
||||
pass
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def get_lock(self, server_id):
|
||||
@ -92,7 +109,7 @@ class PollControls:
|
||||
|
||||
if short is None:
|
||||
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>`'
|
||||
await self.say_error(ctx, error)
|
||||
else:
|
||||
@ -129,7 +146,7 @@ class PollControls:
|
||||
if short is None:
|
||||
pre = await get_server_pre(self.bot, ctx.message.server)
|
||||
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)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
@ -201,8 +218,8 @@ class PollControls:
|
||||
|
||||
if short is None:
|
||||
pre = await get_server_pre(self.bot, ctx.message.server)
|
||||
error = f'Please specify the label of a poll after the close command. \n' \
|
||||
f'`{pre}close <poll_label>`'
|
||||
error = f'Please specify the label of a poll after the export command. \n' \
|
||||
f'`{pre}export <poll_label>`'
|
||||
await self.say_error(ctx, error)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
@ -253,7 +270,7 @@ class PollControls:
|
||||
else:
|
||||
return
|
||||
|
||||
def item_fct(i,item):
|
||||
def item_fct(i, item):
|
||||
return f':black_small_square: **{item["short"]}**: {item["name"]}'
|
||||
|
||||
title = f' Listing {short} polls'
|
||||
@ -281,78 +298,81 @@ class PollControls:
|
||||
@commands.command(pass_context=True)
|
||||
async def cmd(self, ctx, *, cmd=None):
|
||||
'''The old, command style way paired with the wizard.'''
|
||||
await self.say_embed(ctx, say_text='This command is temporarily disabled.')
|
||||
# server = await ask_for_server(self.bot, ctx.message)
|
||||
# if not server:
|
||||
# return
|
||||
# pre = await get_server_pre(self.bot, server)
|
||||
#
|
||||
# # generate the argparser and handle invalid stuff
|
||||
# descr = 'Accept poll settings via commandstring. \n\n' \
|
||||
# '**Wrap all arguments in quotes like this:** \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. ' \
|
||||
# 'If an argument is invalid, the wizard will step in. ' \
|
||||
# 'If the command string is invalid, you will get this error :)'
|
||||
# parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False)
|
||||
# parser.add_argument('-question', '-q')
|
||||
# parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id)))
|
||||
# parser.add_argument('-options', '-o')
|
||||
# parser.add_argument('-multiple_choice', '-mc', default='1')
|
||||
# parser.add_argument('-roles', '-r', default='all')
|
||||
# parser.add_argument('-weights', '-w', default='none')
|
||||
# parser.add_argument('-deadline', '-d', default='0')
|
||||
# parser.add_argument('-anonymous', '-a', action="store_true")
|
||||
#
|
||||
# helpstring = parser.format_help()
|
||||
# helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
|
||||
#
|
||||
# if cmd and cmd == 'help':
|
||||
# await self.say_embed(ctx, say_text=helpstring)
|
||||
# return
|
||||
#
|
||||
# try:
|
||||
# cmds = shlex.split(cmd)
|
||||
# except ValueError:
|
||||
# await self.say_error(ctx, error_text=helpstring)
|
||||
# return
|
||||
# except:
|
||||
# return
|
||||
#
|
||||
# try:
|
||||
# args, unknown_args = parser.parse_known_args(cmds)
|
||||
# except SystemExit:
|
||||
# await self.say_error(ctx, error_text=helpstring)
|
||||
# return
|
||||
# except:
|
||||
# return
|
||||
#
|
||||
# if unknown_args:
|
||||
# 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'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \
|
||||
# f'\n\nHere are the arguments I could not understand:\n'
|
||||
# error_text += '`'+'\n'.join(unknown_args)+'`'
|
||||
# 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()]) + '`'
|
||||
#
|
||||
# await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.')
|
||||
# return
|
||||
#
|
||||
# # pass arguments to the wizard
|
||||
# async def route(poll):
|
||||
# await poll.set_name(force=args.question)
|
||||
# await poll.set_short(force=args.label)
|
||||
# await poll.set_anonymous(force=f'{"yes" if args.anonymous else "no"}')
|
||||
# await poll.set_options_reaction(force=args.options)
|
||||
# await poll.set_multiple_choice(force=args.multiple_choice)
|
||||
# await poll.set_roles(force=args.roles)
|
||||
# await poll.set_weights(force=args.weights)
|
||||
# await poll.set_duration(force=args.deadline)
|
||||
#
|
||||
# poll = await self.wizard(ctx, route, server)
|
||||
# if poll:
|
||||
# await poll.post_embed(destination=poll.channel)
|
||||
# await self.say_embed(ctx, say_text='This command is temporarily disabled.')
|
||||
server = await ask_for_server(self.bot, ctx.message)
|
||||
if not server:
|
||||
return
|
||||
pre = await get_server_pre(self.bot, server)
|
||||
try:
|
||||
# generate the argparser and handle invalid stuff
|
||||
descr = 'Accept poll settings via commandstring. \n\n' \
|
||||
'**Wrap all arguments in quotes like this:** \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. ' \
|
||||
'If an argument is invalid, the wizard will step in. ' \
|
||||
'If the command string is invalid, you will get this error :)'
|
||||
parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False)
|
||||
parser.add_argument('-question', '-q')
|
||||
parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id)))
|
||||
parser.add_argument('-options', '-o')
|
||||
parser.add_argument('-multiple_choice', '-mc', default='1')
|
||||
parser.add_argument('-roles', '-r', default='all')
|
||||
parser.add_argument('-weights', '-w', default='none')
|
||||
parser.add_argument('-deadline', '-d', default='0')
|
||||
parser.add_argument('-anonymous', '-a', action="store_true")
|
||||
|
||||
helpstring = parser.format_help()
|
||||
helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
|
||||
|
||||
if cmd and cmd == 'help':
|
||||
await self.say_embed(ctx, say_text=helpstring)
|
||||
return
|
||||
|
||||
try:
|
||||
cmds = shlex.split(cmd)
|
||||
except ValueError:
|
||||
await self.say_error(ctx, error_text=helpstring)
|
||||
return
|
||||
except:
|
||||
return
|
||||
|
||||
try:
|
||||
args, unknown_args = parser.parse_known_args(cmds)
|
||||
except SystemExit:
|
||||
await self.say_error(ctx, error_text=helpstring)
|
||||
return
|
||||
except:
|
||||
return
|
||||
|
||||
if unknown_args:
|
||||
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'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \
|
||||
f'\n\nHere are the arguments I could not understand:\n'
|
||||
error_text += '`'+'\n'.join(unknown_args)+'`'
|
||||
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()]) + '`'
|
||||
|
||||
await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.')
|
||||
return
|
||||
|
||||
# pass arguments to the wizard
|
||||
async def route(poll):
|
||||
await poll.set_name(force=args.question)
|
||||
await poll.set_short(force=args.label)
|
||||
await poll.set_anonymous(force=f'{"yes" if args.anonymous else "no"}')
|
||||
await poll.set_options_reaction(force=args.options)
|
||||
await poll.set_multiple_choice(force=args.multiple_choice)
|
||||
await poll.set_roles(force=args.roles)
|
||||
await poll.set_weights(force=args.weights)
|
||||
await poll.set_duration(force=args.deadline)
|
||||
|
||||
poll = await self.wizard(ctx, route, server)
|
||||
if poll:
|
||||
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)
|
||||
@ -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...
|
||||
message_id = data.get('message_id')
|
||||
user_id = data.get('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)]
|
||||
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)]
|
||||
return
|
||||
|
||||
|
||||
# check if we can find a poll label
|
||||
channel_id = data.get('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:
|
||||
# discord rapidly closes dm channels by desing
|
||||
# put private channels back into the bots cache and try again
|
||||
await self.bot.start_private_message(user)
|
||||
channel = self.bot.get_channel(channel_id)
|
||||
|
||||
message = await self.bot.get_message(channel=channel, id=message_id)
|
||||
label = None
|
||||
if message and message.embeds:
|
||||
@ -508,28 +536,22 @@ class PollControls:
|
||||
if not label:
|
||||
return
|
||||
|
||||
# fetch poll
|
||||
# create message object for the reaction sender, to get correct server
|
||||
if not server_id:
|
||||
user_msg = copy.deepcopy(message)
|
||||
user_msg.author = user
|
||||
server = await ask_for_server(self.bot, user_msg, label)
|
||||
server_id = server.id
|
||||
|
||||
# this is exclusive
|
||||
|
||||
lock = self.get_lock(server.id)
|
||||
lock = self.get_lock(server_id)
|
||||
async with lock:
|
||||
# try to load poll form cache
|
||||
p = self.bot.poll_cache.get(str(server.id) + label)
|
||||
if not p:
|
||||
p = await Poll.load_from_db(self.bot, server.id, label)
|
||||
p = await Poll.load_from_db(self.bot, server_id, label)
|
||||
if not isinstance(p, Poll):
|
||||
return
|
||||
|
||||
if not p.anonymous:
|
||||
# for anonymous polls we can't unvote because we need to hide reactions
|
||||
member = server.get_member(user_id)
|
||||
await p.unvote(member, emoji, message, lock)
|
||||
|
||||
await p.unvote(user, emoji, message, lock)
|
||||
|
||||
async def do_on_reaction_add(self, data):
|
||||
# dont look at bot's own reactions
|
||||
@ -576,8 +598,6 @@ class PollControls:
|
||||
# hopefully it will scale well enough or I need a different solution
|
||||
lock = self.get_lock(server.id)
|
||||
async with lock:
|
||||
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):
|
||||
return
|
||||
@ -624,7 +644,8 @@ class PollControls:
|
||||
if user.id in p.votes:
|
||||
if p.votes[user.id]['choices'].__len__() > 0:
|
||||
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(
|
||||
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
|
||||
@ -646,11 +667,44 @@ class PollControls:
|
||||
elif deadline == 0:
|
||||
time_left = 'Until manually closed.'
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
if str(channel.type) != 'private' and p.anonymous:
|
||||
# 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)
|
||||
|
||||
# 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?
|
||||
# for now MC 1 is like MC x
|
||||
# if str(channel.type) != 'private' and p.multiple_choice == 1:
|
||||
# # remove all other reactions
|
||||
# # if lock._waiters.__len__() == 0:
|
||||
# for r in message.reactions:
|
||||
# if r.emoji and r.emoji != emoji:
|
||||
# await self.bot.remove_reaction(message, r.emoji, user)
|
||||
# pass
|
||||
if str(channel.type) != 'private' and p.multiple_choice == 1:
|
||||
# remove all other reactions
|
||||
# if lock._waiters.__len__() == 0:
|
||||
for r in message.reactions:
|
||||
if r.emoji and r.emoji != emoji:
|
||||
await self.bot.remove_reaction(message, r.emoji, user)
|
||||
pass
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
||||
@ -100,8 +100,12 @@ async def ask_for_channel(bot, server, message):
|
||||
if str(message.channel.type) == 'text':
|
||||
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
|
||||
channel_list = [c for c in server.channels if str(c.type) == 'text']
|
||||
if channel_list.__len__() == 1:
|
||||
return channel_list[0]
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ async def on_ready():
|
||||
bot.db = mongo.pollmaster
|
||||
bot.session = aiohttp.ClientSession()
|
||||
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
|
||||
try:
|
||||
@ -69,11 +69,12 @@ async def on_ready():
|
||||
{'$set': {'prefix': 'pm!', 'admin_role': 'polladmin', 'user_role': 'polluser'}},
|
||||
upsert=True
|
||||
)
|
||||
try:
|
||||
await import_old_database(bot, server)
|
||||
print(str(server), "updated.")
|
||||
except:
|
||||
print(str(server.id), "failed.")
|
||||
# stopping migration support.
|
||||
# try:
|
||||
# await import_old_database(bot, server)
|
||||
# print(str(server), "updated.")
|
||||
# except:
|
||||
# print(str(server.id), "failed.")
|
||||
except:
|
||||
print("Problem verifying servers.")
|
||||
|
||||
@ -82,9 +83,8 @@ async def on_ready():
|
||||
|
||||
# global locks and caches for performance when voting rapidly
|
||||
bot.locks = {}
|
||||
bot.poll_cache = {}
|
||||
# bot.poll_refresh_q = {}
|
||||
bot.poll_refresh_q = UniqueQueue()
|
||||
# bot.poll_cache = {}
|
||||
# bot.poll_refresh_q = UniqueQueue()
|
||||
|
||||
print("Servers verified. Bot running.")
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Pollmaster V 2.1
|
||||
# Pollmaster V 2.2
|
||||
|
||||
## Overview
|
||||
|
||||
@ -34,6 +34,7 @@ Here is how Pollmaster looks in action:
|
||||
| pm!help | Shows an interactive help menu |
|
||||
| pm!new | Starts a new poll with all the settings |
|
||||
| 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!userrole <any role> | Set the role that has the rights to use the bot |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user