Merge pull request #22 from matnad/survey

Survey
This commit is contained in:
Matthias Nadler 2019-06-18 18:33:14 +02:00 committed by GitHub
commit d8feff3448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 787 additions and 225 deletions

3
.gitignore vendored
View File

@ -14,3 +14,6 @@ cogs/__pycache__/
essentials/__pycache__/
utils/__pycache__/
# export
export/*

57
cogs/admin.py Normal file
View File

@ -0,0 +1,57 @@
import logging
from discord.ext import commands
class Admin(commands.Cog):
def __init__(self, bot):
self.bot = bot
# every commands needs owner permissions
async def cog_check(self, ctx):
return self.bot.owner == ctx.author
async def cog_command_error(self, ctx, error):
if isinstance(error, commands.CheckFailure):
await ctx.send("Only the owner can use this module. Join the support discord server if you are having "
"any problems. This usage has been logged.")
logger.warning(f'User {ctx.author} ({ctx.author.id}) has tried to access a restricted '
f'command via {ctx.message.content}.')
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send("Missing a required argument for this command.")
else:
logger.warning(error)
@commands.command(aliases=['r'])
async def reload(self, ctx, *, cog):
logger.info(f'Trying to reload cog: cogs.{cog}.')
reply = ''
try:
self.bot.reload_extension('cogs.'+cog)
reply = f'Extension "cogs.{cog}" successfully reloaded.'
except commands.ExtensionNotFound:
reply = f'Extension "cogs.{cog}" not found.'
except commands.NoEntryPointError:
reply = f'Extension "cogs.{cog}" is missing a setup function.'
except commands.ExtensionFailed:
reply = f'Extension "cogs.{cog}" failed to start.'
except commands.ExtensionNotLoaded:
reply = f'Extension "cogs.{cog}" is not loaded... trying to load it. '
try:
self.bot.load_extension('cogs.'+cog)
except commands.ExtensionAlreadyLoaded:
reply += f'Could not load or reload extension since it is already loaded...'
except commands.ExtensionNotFound:
reply += f'Extension "cogs.{cog}" not found.'
except commands.ExtensionFailed:
reply = f'Extension "cogs.{cog}" failed to start.'
finally:
logger.info(reply)
await ctx.send(reply)
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(Admin(bot))

View File

@ -25,6 +25,10 @@ class DiscordBotsOrgAPI(commands.Cog):
if SETTINGS.mode == 'production':
await self.dblpy.post_server_count()
logger.info('posted server count ({})'.format(len(self.bot.guilds)))
sum_users = 0
for guild in self.bot.guilds:
sum_users += len(guild.members)
logger.info(f'total users served by the bot: {sum_users}')
except Exception as e:
logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e))
await asyncio.sleep(1800)

View File

@ -12,11 +12,10 @@ class Help(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.pages = ['🏠', '🆕', '🔍', '🕹', '🛠', '💖']
self.pages = ['🏠', '🆕', '🔍', '🕹', '🛠', '', '💖']
async def embed_list_reaction_handler(self, ctx, page, pre, msg=None):
embed = self.get_help_embed(page, pre)
if msg is None:
msg = await ctx.send(embed=embed)
# add reactions
@ -39,8 +38,6 @@ class Help(commands.Cog):
await reaction.message.remove_reaction(reaction.emoji, user)
return reaction
def get_help_embed(self, page, pre):
title = f' Pollmaster Help - React with an emoji to learn more about a topic!'
@ -49,9 +46,10 @@ class Help(commands.Cog):
embed.set_footer(text='Use reactions to navigate the help. This message will self-destruct in 3 minutes.')
if page == '🏠':
## POLL CREATION SHORT
# POLL CREATION SHORT
embed.add_field(name='🆕 Making New Polls',
value=f'`{pre}quick` | `{pre}new` | `{pre}prepare` | `{pre}cmd <args>`', inline=False)
value=f'`{pre}quick` | `{pre}new` | `{pre}advanced` | `{pre}prepare` | `{pre}cmd <args>`',
inline=False)
# embed.add_field(name='Commands', value=f'`{pre}quick` | `{pre}new` | `{pre}prepared`', inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: `<poll question>` (optional)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}new` | `{pre}quick What is the greenest color?`',
@ -59,63 +57,72 @@ class Help(commands.Cog):
## POLL CONTROLS
embed.add_field(name='🔍 Show Polls',
value=f'`{pre}show (label)`', inline=False)
value=f'`{pre}show` | `{pre}show <label>` | `{pre}show <category>`', inline=False)
# embed.add_field(name='Command', value=f'`{pre}show (label)`', inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: `open` (default) | `closed` | `prepared` | '
# f'`<poll_label>` (optional)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}show` | `{pre}show closed` | `{pre}show mascot`',
# inline=False)
## POLL CONTROLS
# POLL CONTROLS
embed.add_field(name='🕹 Poll Controls',
value=f'`{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ', inline=False)
value=f'`{pre}copy` | `{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ',
inline=False)
# embed.add_field(name='Commands', value=f'`{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ',
# inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: <poll_label> (required)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}close mascot` | `{pre}export proposal`',
# inline=False)
## POLL CONTROLS
# POLL CONTROLS
embed.add_field(name='🛠 Configuration',
value=f'`{pre}userrole (role)` | `{pre}adminrole (role)` | `{pre}prefix <new_prefix>` ',
value=f'`{pre}userrole [role]` | `{pre}adminrole [role]` | `{pre}prefix <new_prefix>` ',
inline=False
)
# embed.add_field(name='Commands',
# value=f'`{pre}userrole <role>` | `{pre}adminrole <role>` | `{pre}prefix <new_prefix>` ',
# inline=False)
## ABOUT
# DEBUGGING
embed.add_field(name='❔ Debugging',
value=f'`@debug` | `@mention` | `@mention <tag>` ',
inline=False
)
# ABOUT
embed.add_field(name='💖 About Pollmaster',
value='More infos about Pollmaster, the developer, where to go for further help and how you can support us.',
inline=False)
elif page == '🆕':
embed.add_field(name='🆕 Making New Polls',
value='There are three ways to create a new poll. For all three commands you can either just '
'type the command or type the command followed by the question to skip that first step.'
'Your Members need the <admin> or <user> role to use these commands. More in 🛠 Configuration.',
value='There are four ways to create a new poll. For all the commands you can either just '
'type the command or type the command followed by the question to skip the first step.'
'Your Members need the <admin> or <user> role to use these commands. '
'More on user rights in 🛠 Configuration.',
inline=False)
embed.add_field(name=f'🔹 **Quick Poll:** `{pre}quick <poll question>` (optional)',
embed.add_field(name=f'🔹 **Quick Poll:** `{pre}quick`',
value='If you just need a quick poll, this is the way to go. All you have to specify is the '
'question and your answers; the rest will be set to default values.',
inline=False)
embed.add_field(name=f'🔹 **All Features:** `{pre}new <poll question>` (optional)',
value='This command gives you full control over your poll. A step by step wizard will guide '
embed.add_field(name=f'🔹 **Basic Poll:** `{pre}new`',
value='This command gives control over the most common settings. A step by step wizard will guide '
'you through the process and you can specify options such as Multiple Choice, '
'Anonymous Voting, Role Restriction, Role Weights and Deadline.',
'Anonymous Voting and Deadline.',
inline=False)
embed.add_field(name=f'🔹 **Prepare and Schedule:** `{pre}prepare <poll question>` (optional)',
value=f'Similar to `{pre}new`, this gives you all the options. But additionally, the poll will '
embed.add_field(name=f'🔹 **Advanced Poll:** `{pre}advanced`',
value='This command gives you full control over your poll. A step by step wizard will guide '
'you through the process and you can specify additional options such as Hide Vote Count, '
'Role Restrictions, Role Weights or Custom Write-In Answers (Survey Flags).',
inline=False)
embed.add_field(name=f'🔹 **Prepare and Schedule:** `{pre}prepare`',
value=f'Similar to `{pre}advanced`, this gives you all the options. But additionally, the poll will '
'be set to \'inactive\'. You can specify if the poll should activate at a certain time '
f'and/or if you would like to manually `{pre}activate` it. '
'Perfect if you are preparing for a team meeting!',
inline=False)
embed.add_field(name=f'🔹 **-Advanced- Commandline:** `{pre}cmd <args>`',
embed.add_field(name=f'🔹 **-Advanced- Commandline:** `{pre}cmd <arguments>`',
value=f'For the full syntax type `{pre}cmd help`\n'
f'Similar to version 1 of the bot, with this command you can create a poll in one message. '
f'Pass all the options you need via command line arguments, the rest will be set to '
f'default values. The wizard will step in for invalid arguments.\n'
f'Example: `{pre}cmd -q "Which colors?" -l colors -o "green, blue, red" -mc -a`',
f'Example: `{pre}cmd -q "Which colors?" -l colors -o "green, blue, red" -h -a`',
inline=False)
elif page == '🔍':
@ -137,9 +144,15 @@ class Help(commands.Cog):
inline=False)
elif page == '🕹':
embed.add_field(name='🕹 Poll Controls',
value='All these commands can only be used by an <admin> or by the author of the poll. '
value='All these commands except copy can only be used by an <admin> or by the author of the poll. '
'Go to 🛠 Configuration for more info on the permissions.',
inline=False)
embed.add_field(name=f'🔹 **Copy** `{pre}copy <poll_label>`',
value='This will give you a cmd string that you can post into any channel to create a copy'
'of the specified poll. It will increment the label and depending on the settings, '
'you might need to add missing information like a new deadline. '
f'\nFor more info, see: `{pre}cmd help`.',
inline=False)
embed.add_field(name=f'🔹 **Close** `{pre}close <poll_label>`',
value='Polls will close automatically when their deadline is reached. But you can always '
'close them manually by using this command. A closed poll will lock in the votes so '
@ -180,6 +193,20 @@ class Help(commands.Cog):
'whitespace, use "\w" instead of " " (discord deletes trailing whitespaces).',
inline=False)
elif page == '':
embed.add_field(name='❔ Debugging',
value='These commands are independent of your server prefix and serve to debug the bot.',
inline=False)
embed.add_field(name=f'🔹 **Debug:** `@debug`',
value='This command will check the required permissions in the channel it is used and'
'generate a short report with suggestions on your next actions.'
'If you are stuck, please visit the support discord server.',
inline=False)
embed.add_field(name=f'🔹 **Mention:** `@mention` | `@mention prefix`',
value='This is a prefix independent command to retrieve your prefix in case you changed '
'and forgot it. More `@mention` tags might be added in the future.',
inline=False)
elif page == '💖':
embed.add_field(name='💖 Pollmaster 💖',
value='If you enjoy the bot, you can show your appreciation by giving him an upvote on Discordbots.',
@ -205,8 +232,15 @@ class Help(commands.Cog):
return embed
@commands.command()
async def help(self, ctx, *, topic=None):
async def help(self, ctx):
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
if not ctx.message.channel.permissions_for(server.me).embed_links:
await ctx.send("Missing permissions. Type \"@debug.\"")
return
pre = await get_server_pre(self.bot, server)
rct = 1
while rct is not None:
@ -217,9 +251,89 @@ class Help(commands.Cog):
page = rct.emoji
msg = rct.message
rct = await self.embed_list_reaction_handler(ctx, page, pre, msg)
# print(res.user, res.reaction, res.reaction.emoji)
# cleanup
await ctx.message.delete()
try:
await ctx.message.delete()
except PermissionError:
pass
# @mention and @debug commands
@commands.Cog.listener()
async def on_message(self, message):
if message.content.startswith("@mention"):
channel = message.channel
if not isinstance(channel, discord.TextChannel):
await channel.send("@mention can only be used in a server text channel.")
return
guild = message.guild
if not guild:
await channel.send("Could not determine your server.")
return
if message.content == "@mention":
await channel.send("The following @mention tags are available:\n🔹 @mention prefix")
return
try:
tag = message.content.split()[1].lower()
except IndexError:
await channel.send("Wrong formatting. Type \"@mention\" or \"@mention <tag>\".")
return
if tag == "prefix":
pre = await get_server_pre(self.bot, guild)
# await channel.send(f'The prefix for this server/channel is: \n {pre} \n To change it type: \n'
# f'{pre}prefix <new_prefix>')
await channel.send(pre)
else:
await channel.send(f'Tag "{tag}" not found. Type "@mention" for a list of tags.')
elif message.content == "@debug":
channel = message.channel
if not isinstance(channel, discord.TextChannel):
await channel.send("@debug can only be used in a server text channel.")
return
guild = message.guild
if not guild:
await channel.send("Could not determine your server.")
return
status_msg = ''
setup_correct = True
# check send message permissions
permissions = channel.permissions_for(guild.me)
if not permissions.send_messages:
await message.author.send(f'I don\'t have permission to send text messages in channel "{channel}" '
f'on server "{guild}"')
return
status_msg += ' ✅ Sending text messages\n'
# check embed link permissions
if permissions.embed_links:
status_msg += '✅ Sending embedded messages\n'
else:
status_msg += '❗ Sending embedded messages. I need permissions to embed links!\n'
setup_correct = False
# check manage messages
if permissions.manage_messages:
status_msg += '✅ Deleting messages and reactions\n'
else:
status_msg += '❗ Deleting messages and reactions. I need the manage messages permission!\n'
setup_correct = False
if setup_correct:
status_msg += 'No action required. Your permissions are set up correctly for this channel. \n' \
'If the bot does not work, feel free to join the support discord server.'
else:
status_msg += 'Please try to fix the issues above. \nIf you are still having problems, ' \
'visit the support discord server.'
await channel.send(status_msg)
def setup(bot):

View File

@ -11,26 +11,27 @@ import pytz
from discord.ext import commands
from utils.misc import CustomFormatter
from .poll import Poll
from models.poll import Poll
from utils.paginator import embed_list_paginated
from essentials.multi_server import get_server_pre, ask_for_server, ask_for_channel
from essentials.settings import SETTINGS
from utils.poll_name_generator import generate_word
from essentials.exceptions import StopWizard
## A-Z Emojis for Discord
# 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(commands.Cog):
def __init__(self, bot):
self.bot = bot
#self.bot.loop.create_task(self.close_polls())
self.bot.loop.create_task(self.close_polls())
self.ignore_next_removed_reaction = {}
#
# # General Methods
def get_label(self, message : discord.Message):
def get_label(self, message: discord.Message):
label = None
if message and message.embeds:
embed = message.embeds[0]
@ -45,6 +46,10 @@ class PollControls(commands.Cog):
"""This function runs every 60 seconds to schedule prepared polls and close expired polls"""
while True:
try:
if not hasattr(self.bot, 'db'):
await asyncio.sleep(30)
continue
query = self.bot.db.polls.find({'active': False, 'activation': {"$not": re.compile("0")}})
if query:
for pd in [poll async for poll in query]:
@ -73,17 +78,17 @@ class PollControls(commands.Cog):
except:
continue
except AttributeError as ae:
#Database not loaded yet
# Database not loaded yet
logger.warning("Attribute Error in close_polls loop")
logger.exception(ae)
pass
except Exception as ex:
#Never break this loop due to an error
# Never break this loop due to an error
logger.error("Other Error in close_polls loop")
logger.exception(ex)
pass
await asyncio.sleep(30)
await asyncio.sleep(60)
def get_lock(self, server_id):
if not self.bot.locks.get(server_id):
@ -119,7 +124,7 @@ class PollControls(commands.Cog):
embed.set_footer(text=footer_text)
await ctx.send(embed=embed)
# # Commands
# Commands
@commands.command()
async def activate(self, ctx, *, short=None):
"""Activate a prepared poll. Parameter: <label>"""
@ -229,6 +234,31 @@ class PollControls(commands.Cog):
await self.say_error(ctx, error)
await ctx.invoke(self.show)
@commands.command()
async def copy(self, ctx, *, short=None):
'''Copy a poll. Parameter: <label>'''
server = await ask_for_server(self.bot, ctx.message, short)
if not server:
return
if short is None:
pre = await get_server_pre(self.bot, ctx.message.guild)
error = f'Please specify the label of a poll after the copy command. \n' \
f'`{pre}copy <poll_label>`'
await self.say_error(ctx, error)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
text = await get_server_pre(self.bot, server) + p.to_command()
await self.say_embed(ctx, text, title="Paste this to create a copy of the poll")
else:
error = f'Poll with label "{short}" was not found. Listing all open polls.'
# pre = await get_server_pre(self.bot, ctx.message.server)
# footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error)
await ctx.invoke(self.show)
@commands.command()
async def export(self, ctx, *, short=None):
'''Export a poll. Parameter: <label>'''
@ -317,87 +347,96 @@ class PollControls(commands.Cog):
pre = await get_server_pre(self.bot, server)
footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error, footer)
#
# @commands.command()
# 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)
# # 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()
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)
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('-anonymous', '-a', action="store_true")
parser.add_argument('-options', '-o')
parser.add_argument('-survey_flags', '-sf', default='0')
parser.add_argument('-multiple_choice', '-mc', default='1')
parser.add_argument('-hide_votes', '-h', action="store_true")
parser.add_argument('-roles', '-r', default='all')
parser.add_argument('-weights', '-w', default='none')
parser.add_argument('-prepare', '-p', default='-1')
parser.add_argument('-deadline', '-d', default='0')
helpstring = parser.format_help()
helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
if not cmd or len(cmd) < 2 or cmd == 'help':
# Shlex will block if the string is empty
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(ctx, force=args.question)
await poll.set_short(ctx, force=args.label)
await poll.set_anonymous(ctx, force=f'{"yes" if args.anonymous else "no"}')
await poll.set_options_reaction(ctx, force=args.options)
await poll.set_survey_flags(ctx, force=args.survey_flags)
await poll.set_multiple_choice(ctx, force=args.multiple_choice)
await poll.set_hide_vote_count(ctx, force=f'{"yes" if args.hide_votes else "no"}')
await poll.set_roles(ctx, force=args.roles)
await poll.set_weights(ctx, force=args.weights)
await poll.set_preparation(ctx, force=args.prepare)
await poll.set_duration(ctx, force=args.deadline)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
except Exception as error:
logger.error("ERROR IN pm!cmd")
logger.exception(error)
@commands.command()
async def quick(self, ctx, *, cmd=None):
'''Create a quick poll with just a question and some options. Parameters: <Question> (optional)'''
@ -411,6 +450,7 @@ class PollControls(commands.Cog):
await poll.set_anonymous(ctx, force='no')
await poll.set_options_reaction(ctx)
await poll.set_multiple_choice(ctx, force='1')
await poll.set_hide_vote_count(ctx, force='no')
await poll.set_roles(ctx, force='all')
await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx, force='0')
@ -432,7 +472,9 @@ class PollControls(commands.Cog):
await poll.set_preparation(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx)
await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx)
await poll.set_roles(ctx)
await poll.set_weights(ctx)
await poll.set_duration(ctx)
@ -441,6 +483,29 @@ class PollControls(commands.Cog):
if poll:
await poll.post_embed(ctx.message.author)
@commands.command()
async def advanced(self, ctx, *, cmd=None):
"""Poll with more options. Parameters: <Question> (optional)"""
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
async def route(poll):
await poll.set_name(ctx, force=cmd)
await poll.set_short(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx)
await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx)
await poll.set_roles(ctx)
await poll.set_weights(ctx)
await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
@commands.command()
async def new(self, ctx, *, cmd=None):
"""Start the poll wizard to create a new poll step by step. Parameters: <Question> (optional)"""
@ -453,9 +518,11 @@ class PollControls(commands.Cog):
await poll.set_short(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx, force='0')
await poll.set_multiple_choice(ctx)
await poll.set_roles(ctx)
await poll.set_weights(ctx)
await poll.set_hide_vote_count(ctx, force='no')
await poll.set_roles(ctx, force='all')
await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server)
@ -468,13 +535,14 @@ class PollControls(commands.Cog):
if not channel:
return
pre = await get_server_pre(self.bot, server)
# Permission Check
member = server.get_member(ctx.message.author.id)
if not member.guild_permissions.manage_guild:
result = await self.bot.db.config.find_one({'_id': str(server.id)})
if result and result.get('admin_role') not in [r.name for r in member.roles] and result.get(
'user_role') not in [r.name for r in member.roles]:
pre = await get_server_pre(self.bot, server)
await ctx.message.author.send('You don\'t have sufficient rights to start new polls on this server. '
'A server administrator has to assign the user or admin role to you. '
f'To view and set the permissions, an admin can use `{pre}userrole` and '
@ -485,35 +553,20 @@ class PollControls(commands.Cog):
poll = Poll(self.bot, ctx, server, channel)
## Route to define object, passed as argument for different constructors
if ctx.message and ctx.message.content and not ctx.message.content.startswith(f'{pre}cmd '):
poll.wizard_messages.append(ctx.message)
try:
await route(poll)
poll.finalize()
await poll.clean_up(ctx.channel)
except StopWizard:
await poll.clean_up(ctx.channel)
return
# Finalize
await poll.save_to_db()
return poll
# # BOT EVENTS (@bot.event)
# async def on_socket_raw_receive(self, raw_msg):
# print(raw_msg)
# if not isinstance(raw_msg, str):
# return
# msg = json.loads(raw_msg)
# type = msg.get("t")
# data = msg.get("d")
# if not data:
# return
# # emoji = data.get("emoji")
# # user_id = data.get("user_id")
# # message_id = data.get("message_id")
# if type == "MESSAGE_REACTION_ADD":
# await self.do_on_reaction_add(data)
# elif type == "MESSAGE_REACTION_REMOVE":
# #await self.do_on_reaction_remove(data)
# pass
@commands.Cog.listener()
async def on_raw_reaction_remove(self, data):
# get emoji symbol
@ -672,6 +725,10 @@ class PollControls(commands.Cog):
description='', color=SETTINGS.color)
embed.set_author(name=f" >> {p.short}", icon_url=SETTINGS.author_icon)
# created by
created_by = server.get_member(int(p.author.id))
embed.add_field(name=f'Created by:', value=f'{created_by if created_by else "<Deleted User>"}', inline=False)
# vote rights
vote_rights = await p.has_required_role(member)
embed.add_field(name=f'{"Can you vote?" if is_open else "Could you vote?"}',
@ -679,7 +736,7 @@ class PollControls(commands.Cog):
# edit rights
edit_rights = False
if str(member.id) == str(p.author):
if str(member.id) == str(p.author.id):
edit_rights = True
elif member.guild_permissions.manage_guild:
edit_rights = True
@ -724,7 +781,7 @@ class PollControls(commands.Cog):
await user.send(embed=embed)
# send current details of who currently voted for what
if not p.anonymous and p.votes.__len__() > 0:
if (not p.open or not p.hide_count) and not p.anonymous and p.votes.__len__() > 0:
msg = '--------------------------------------------\n' \
'CURRENT VOTES\n' \
'--------------------------------------------\n'
@ -734,17 +791,21 @@ class PollControls(commands.Cog):
msg += "**" +o+":**"
c = 0
for user_id in p.votes:
member = server.get_member(user_id)
member = server.get_member(int(user_id))
if not member or i not in p.votes[str(user_id)]['choices']:
continue
c += 1
name = member.nick
if not name:
name = member.name
if not name:
name = "<Deleted User>"
msg += f'\n{name}'
if p.votes[str(user_id)]['weight'] != 1:
msg += f' (weight: {p.votes[str(user_id)]["weight"]})'
# msg += ': ' + ', '.join([AZ_EMOJIS[c]+" "+p.options_reaction[c] for c in p.votes[user_id]['choices']])
if i in p.survey_flags:
msg += f': {p.votes[str(user_id)]["answers"][p.survey_flags.index(i)]}'
if msg.__len__() > 1500:
await user.send(msg)
msg = ''
@ -754,6 +815,29 @@ class PollControls(commands.Cog):
if msg.__len__() > 0:
await user.send(msg)
elif (not p.open or not p.hide_count) and p.anonymous and p.survey_flags.__len__() > 0 and p.votes.__len__() > 0:
msg = '--------------------------------------------\n' \
'Custom Answers (Anonymous)\n' \
'--------------------------------------------\n'
has_answers = False
for i, o in enumerate(p.options_reaction):
if i not in p.survey_flags:
continue
custom_answers = ''
for user_id in p.votes:
if i in p.votes[str(user_id)]["choices"]:
has_answers = True
custom_answers += f'\n{p.votes[str(user_id)]["answers"][p.survey_flags.index(i)]}'
if custom_answers.__len__() > 0:
msg += AZ_EMOJIS[i] + " "
msg += "**" + o + ":**"
msg += custom_answers
msg += '\n\n'
if msg.__len__() > 1500:
await user.send(msg)
msg = ''
if has_answers and msg.__len__() > 0:
await user.send(msg)
return
# Assume: User wants to vote with reaction
@ -770,7 +854,6 @@ class PollControls(commands.Cog):
self.ignore_next_removed_reaction[str(message.id) + str(emoji)] = user_id
asyncio.ensure_future(message.remove_reaction(emoji, user))
# order here is crucial since we can't determine if a reaction was removed by the bot or user
# update database with vote
await p.vote(member, emoji, message, lock)
@ -785,6 +868,7 @@ class PollControls(commands.Cog):
# await message.remove_reaction(r.emoji, user)
# pass
def setup(bot):
global logger
logger = logging.getLogger('bot')

View File

@ -21,7 +21,7 @@ async def get_pre(bot, message):
async def get_server_pre(bot, server):
'''Gets the prefix for a server.'''
"""Gets the prefix for a server."""
try:
#result = await bot.db.config.find_one({'_id': str(server.id)})
result = bot.pre[str(server.id)]
@ -33,7 +33,7 @@ async def get_server_pre(bot, server):
async def get_servers(bot, message, short=None):
'''Get best guess of relevant shared servers'''
"""Get best guess of relevant shared servers"""
if message.guild is None:
list_of_shared_servers = []
for s in bot.guilds:
@ -148,7 +148,7 @@ async def ask_for_channel(ctx, bot, server, message):
def check(m):
return message.author.id == m.author.id
try:
reply = await bot.wait_for('message', timeout=60)
reply = await bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError:
pass
else:
@ -161,4 +161,4 @@ async def ask_for_channel(ctx, bot, server, message):
nr = int(reply.content)
if 0 < nr <= channel_list.__len__():
valid_reply = True
return channel_list[nr - 1]
return channel_list[nr - 1]

0
models/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

View File

@ -3,7 +3,8 @@ import codecs
import datetime
import logging
import os
import re
import random
import dateparser
import pytz
import regex
@ -23,13 +24,13 @@ from utils.misc import possible_timezones
logger = logging.getLogger('bot')
## Helvetica is the closest font to Whitney (discord uses Whitney) in afm
## This is used to estimate text width and adjust the layout of the embeds
# Helvetica is the closest font to Whitney (discord uses Whitney) in afm
# This is used to estimate text width and adjust the layout of the embeds
afm_fname = os.path.join(rcParams['datapath'], 'fonts', 'afm', 'phvr8a.afm')
with open(afm_fname, 'rb') as fh:
afm = AFM(fh)
## A-Z Emojis for Discord
# 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)]
@ -57,10 +58,12 @@ class Poll:
self.name = "Quick Poll"
self.short = str(uuid4())[0:23]
self.anonymous = False
self.hide_count = False
self.reaction = True
self.multiple_choice = 1
self.options_reaction = ['yes', 'no']
self.options_reaction_default = False
self.survey_flags = []
# self.options_traditional = []
# self.options_traditional_default = False
self.roles = ['@everyone']
@ -76,6 +79,18 @@ class Poll:
self.activation_tz = 0.0
self.votes = {}
self.wizard_messages = []
def get_preset_options(self, number):
if number == 1:
return ['', '']
elif number == 2:
return ['👍', '🤐', '👎']
elif number == 3:
return ['😍', '👍', '🤐', '👎', '🤢']
elif number == 4:
return ['in favour', 'against', 'abstaining']
async def is_open(self, update_db=True):
if self.server is None:
self.open = True
@ -102,7 +117,9 @@ class Poll:
embed = discord.Embed(title="Poll creation Wizard", description=text, color=SETTINGS.color)
if footer:
embed.set_footer(text="Type `stop` to cancel the wizard.")
return await ctx.send(embed=embed)
msg = await ctx.send(embed=embed)
self.wizard_messages.append(msg)
return msg
async def wizard_says_edit(self, message, text, add=False):
if add and message.embeds.__len__() > 0:
@ -127,9 +144,13 @@ class Poll:
"""Pre-parse user input for wizard"""
def check(m):
return m.author == self.author
try:
reply = await self.bot.wait_for('message', timeout=180, check=check)
except asyncio.TimeoutError:
raise StopWizard
reply = await self.bot.wait_for('message', check=check)
if reply and reply.content:
self.wizard_messages.append(reply)
if reply.content.startswith(await get_pre(self.bot, reply)):
await self.wizard_says(ctx, f'You can\'t use bot commands during the Poll Creation Wizard.\n'
f'Stopping the Wizard and then executing the command:\n`{reply.content}`',
@ -144,7 +165,8 @@ class Poll:
else:
raise InvalidInput
def sanitize_string(self, string):
@staticmethod
def sanitize_string(string):
"""Sanitize user input for wizard"""
# sanitize input
if string is None:
@ -239,7 +261,6 @@ class Poll:
await self.add_error(message,
f'**The label `{reply}` is not unique on this server. Choose a different one!**')
async def set_preparation(self, ctx, force=None):
"""Set the preparation conditions for the Poll."""
async def get_valid(in_reply):
@ -269,6 +290,9 @@ class Poll:
raise DateOutOfRange(dt)
return dt
if str(force) == '-1':
return
try:
dt = await get_valid(force)
self.activation = dt
@ -302,7 +326,7 @@ class Poll:
self.active = False
break
except InvalidInput:
await self.add_error(message, '**I could not understand that format.**')
await self.add_error(message, '**Specify the activation time in a format i can understand.**')
except TypeError:
await self.add_error(message, '**Type Error.**')
except DateOutOfRange as e:
@ -404,7 +428,6 @@ class Poll:
except OutOfRange:
await self.add_error(message, '**You can\'t have more choices than options.**')
async def set_options_reaction(self, ctx, force=None):
"""Set the answers / options of the Poll."""
async def get_valid(in_reply):
@ -418,7 +441,7 @@ class Poll:
return int(split[0])
else:
raise WrongNumberOfArguments
elif 1 < split.__len__() <= 26:
elif 1 < split.__len__() <= 18:
split = [self.sanitize_string(o) for o in split]
if any([len(o) < 1 for o in split]):
raise InvalidInput
@ -427,22 +450,11 @@ class Poll:
else:
raise WrongNumberOfArguments
def get_preset_options(number):
if number == 1:
return ['', '']
elif number == 2:
return ['👍', '🤐', '👎']
elif number == 3:
return ['😍', '👍', '🤐', '👎', '🤢']
elif number == 4:
return ['in favour', 'against', 'abstaining']
try:
options = await get_valid(force)
self.options_reaction_default = False
if isinstance(options, int):
self.options_reaction = get_preset_options(options)
self.options_reaction = self.get_preset_options(options)
if options <= 3:
self.options_reaction_default = True
else:
@ -473,7 +485,7 @@ class Poll:
options = await get_valid(reply)
self.options_reaction_default = False
if isinstance(options, int):
self.options_reaction = get_preset_options(options)
self.options_reaction = self.get_preset_options(options)
if options <= 3:
self.options_reaction_default = True
else:
@ -483,11 +495,115 @@ class Poll:
except InvalidInput:
await self.add_error(message,
'**Invalid entry. Type `1`, `2`, `3` or `4` or a comma separated list of '
'up to 26 options.**')
'up to 18 options.**')
except WrongNumberOfArguments:
await self.add_error(message,
'**You need more than 1 option! Type them in a comma separated list.**')
'**You need more than 1 and less than 19 options! '
'Type them in a comma separated list.**')
async def set_survey_flags(self, ctx, force=None):
"""Decide which Options will ask for user input."""
async def get_valid(in_reply):
if not in_reply:
raise InvalidInput
split = [r.strip() for r in in_reply.split(",")]
if not split or split.__len__() == 1 and split[0] == '0':
return []
if not all([r.isdigit() for r in split]):
raise ExpectedInteger
if any([1 > int(r) or int(r) > len(self.options_reaction) for r in split]):
raise OutOfRange
return [int(r)-1 for r in split]
if self.options_reaction_default:
return
try:
self.survey_flags = await get_valid(force)
return
except InputError:
pass
text = ("**Which options should ask the user for a custom answer?**\n"
"Type `0` to skip survey options.\n"
"If you want multiple survey options, separate the numbers with a comma.\n"
"\n"
"`0 - None (classic poll)`\n"
)
for i, option in enumerate(self.options_reaction):
text += f'`{i + 1} - {option}`\n'
text += ("\n"
"If the user votes for one of these options, the bot will PM them and ask them to provide a text "
"input. You can use this to do surveys or to gather feedback for example.\n")
message = await self.wizard_says(ctx, text)
while True:
try:
if force:
reply = force
force = None
else:
reply = await self.get_user_reply(ctx)
self.survey_flags = await get_valid(reply)
await self.add_vaild(
message, f'{"None" if self.survey_flags.__len__() == 0 else ", ".join(str(f + 1) for f in self.survey_flags)}'
)
break
except InvalidInput:
await self.add_error(message, '**I can\'t read this input.**')
except ExpectedInteger:
await self.add_error(message, '**Only type positive numbers separated by a comma.**')
except OutOfRange:
await self.add_error(message, '**Only type numbers you can see in the list.**')
async def set_hide_vote_count(self, ctx, force=None):
"""Determine the live vote count is hidden or shown."""
async def get_valid(in_reply):
if not in_reply:
raise InvalidInput
is_true = ['yes', '1']
is_false = ['no', '0']
in_reply = self.sanitize_string(in_reply)
if not in_reply:
raise InvalidInput
elif in_reply.lower() in is_true:
return True
elif in_reply.lower() in is_false:
return False
else:
raise InvalidInput
try:
self.hide_count = await get_valid(force)
return
except InputError:
pass
text = ("**Do you want to hide the live vote count?**\n"
"\n"
"`0 - No, show it (Default)`\n"
"`1 - Yes, hide it`\n"
"\n"
"You will still be able to see the vote count once the poll is closed. This settings will just hide "
"the vote count while the poll is active.")
message = await self.wizard_says(ctx, text)
while True:
try:
if force:
reply = force
force = None
else:
reply = await self.get_user_reply(ctx)
self.hide_count = await get_valid(reply)
await self.add_vaild(message, f'{"Yes" if self.hide_count else "No"}')
break
except InvalidInput:
await self.add_error(message, '**You can only answer with `yes` | `1` or `no` | `0`!**')
async def set_roles(self, ctx, force=None):
"""Set role restrictions for the Poll."""
@ -500,7 +616,7 @@ class Poll:
if split.__len__() == 1 and split[0] in ['0', 'all', 'everyone']:
return ['@everyone']
if n_roles <= 20 and force is None:
if n_roles <= 20 and not force:
if not all([r.isdigit() for r in split]):
raise ExpectedInteger
elif any([int(r) > n_roles for r in split]):
@ -614,6 +730,7 @@ class Poll:
force = None
else:
reply = await self.get_user_reply(ctx)
print(reply)
w_n = await get_valid(reply, self.server.roles)
self.weights_roles = w_n[0]
self.weights_numbers = w_n[1]
@ -690,7 +807,7 @@ class Poll:
await self.add_vaild(message, self.duration.strftime('%d-%b-%Y %H:%M %Z'))
break
except InvalidInput:
await self.add_error(message, '**I could not understand that format.**')
await self.add_error(message, '**Specify the deadline in a format I can understand.**')
except TypeError:
await self.add_error(message, '**Type Error.**')
except DateOutOfRange as e:
@ -699,6 +816,85 @@ class Poll:
def finalize(self):
self.time_created = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
async def clean_up(self, channel):
if isinstance(channel, discord.TextChannel):
await channel.delete_messages(self.wizard_messages)
async def ask_for_input_DM(self, user, title, text):
embed = discord.Embed(title=title, description=text, color=SETTINGS.color)
embed.set_footer(text='You can answer anywhere.')
message = await user.send(embed=embed)
def check(m):
return m.author == user
try:
reply = await self.bot.wait_for('message', timeout=120, check=check)
if reply and reply.content:
reply = reply.content
else:
return None
except asyncio.TimeoutError:
if message.embeds.__len__() > 0:
embed.description = embed.description + '\n\n:exclamation: Request timed out. Vote was counted, ' \
'but no custom answer recorded.'
await message.edit(embed=embed)
return None
try:
reply = self.sanitize_string(reply)
except InvalidInput:
embed = discord.Embed(title=title,
description="Invalid Input. To try again, un-vote and re-vote the option.",
color=SETTINGS.color
)
await user.send(embed=embed)
if message.embeds.__len__() > 0:
embed.description = embed.description + '\n\n' + reply
await message.edit(embed=embed)
return reply
def to_command(self):
# make new label by increasing a counter at the end
try:
new_nr = int(self.short[-1]) + 1
new_label = self.short[:-1] + str(new_nr)
except ValueError:
new_label = self.short + "2"
cmd = "cmd"
cmd += " -q \"" + self.name + "\""
cmd += " -l \"" + new_label + "\""
if self.anonymous:
cmd += " -a"
if self.options_reaction_default:
for i in range(1, 5):
if self.get_preset_options(i) == self.options_reaction:
cmd += " -o \"" + str(i) + "\""
else:
cmd += " -o \"" + ", ".join(self.options_reaction) + "\""
if self.survey_flags:
cmd += " -sf \"" + ", ".join([str(x+1) for x in self.survey_flags]) + "\""
cmd += " -mc \"" + str(self.multiple_choice) + "\""
if self.hide_count:
cmd += " -h"
if self.roles != ["@everyone"]:
cmd += " -r \"" + ", ".join(self.roles) + "\""
if not self.active:
cmd += " -p \"specify activation time\""
if self.duration == 0:
cmd += " -d \"0\""
else:
cmd += " -d \"specify deadline\""
return cmd
async def to_dict(self):
if self.channel is None:
cid = 0
@ -715,11 +911,13 @@ class Poll:
'name': self.name,
'short': self.short,
'anonymous': self.anonymous,
'hide_count': self.hide_count,
'reaction': self.reaction,
'multiple_choice': self.multiple_choice,
'options_reaction': self.options_reaction,
'reaction_default': self.options_reaction_default,
#'options_traditional': self.options_traditional,
'survey_flags': self.survey_flags,
'roles': self.roles,
'weights_roles': self.weights_roles,
'weights_numbers': self.weights_numbers,
@ -766,7 +964,7 @@ class Poll:
f'Question / Name: {self.name}\n'
f'Label: {self.short}\n'
f'Anonymous: {"Yes" if self.anonymous else "No"}\n'
f'Multiple choice: {"Yes" if self.multiple_choice else "No"}\n'
f'# Choices: {"Multiple" if self.multiple_choice == 0 else self.multiple_choice}\n'
f'Answer options: {", ".join(self.options_reaction)}\n'
f'Allowed roles: {", ".join(self.roles) if self.roles.__len__() > 0 else "@everyone"}\n'
f'Weights for roles: {weight_str}\n'
@ -795,10 +993,21 @@ class Poll:
if not name:
name = member.name
export += f'\n{name}'
export += f'\n{name}: '
if self.votes[str(user_id)]['weight'] != 1:
export += f' (weight: {self.votes[str(user_id)]["weight"]})'
export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[str(user_id)]['choices']])
# export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[str(user_id)]['choices']])
choice_text_list = []
for choice in self.votes[str(user_id)]['choices']:
choice_text = self.options_reaction[choice]
if choice in self.survey_flags:
choice_text += " ("\
+ self.votes[str(user_id)]["answers"][self.survey_flags.index(choice)] \
+ ") "
choice_text_list.append(choice_text)
if choice_text_list:
export += ', '.join(choice_text_list)
export += '\n'
else:
export += '--------------------------------------------\n' \
@ -821,6 +1030,28 @@ class Poll:
# export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[str(user_id)]['choices']])
export += '\n'
if self.survey_flags.__len__() > 0:
export += '--------------------------------------------\n' \
'CUSTOM ANSWERS (RANDOM ORDER)\n' \
'--------------------------------------------'
for i, o in enumerate(self.options_reaction):
if i not in self.survey_flags:
continue
custom_answers = []
for user_id in self.votes:
if i in self.votes[str(user_id)]["choices"]:
custom_answers.append(f'\n{self.votes[str(user_id)]["answers"][self.survey_flags.index(i)]}')
export += "\n" + o + ":"
if custom_answers.__len__() > 0:
random.shuffle(custom_answers) # randomize answers per question
for answer in custom_answers:
export += answer
export += '\n'
else:
export += "No custom answers were submitted."
export += '\n'
export += ('--------------------------------------------\n'
'BOT DETAILS\n'
'--------------------------------------------\n'
@ -854,6 +1085,13 @@ class Poll:
self.name = d['name']
self.short = d['short']
self.anonymous = d['anonymous']
# backwards compatibility
if 'hide_count' in d.keys():
self.hide_count = d['hide_count']
else:
self.hide_count = False
self.reaction = d['reaction']
# backwards compatibility for multiple choice
@ -872,6 +1110,13 @@ class Poll:
self.options_reaction = d['options_reaction']
self.options_reaction_default = d['reaction_default']
# self.options_traditional = d['options_traditional']
# backwards compatibility
if 'survey_flags' in d.keys():
self.survey_flags = d['survey_flags']
else:
self.survey_flags = []
self.roles = d['roles']
self.weights_roles = d['weights_roles']
self.weights_numbers = d['weights_numbers']
@ -945,18 +1190,20 @@ class Poll:
embed = await self.add_field_custom(name='**Poll Question**', value=self.name, embed=embed)
embed = await self.add_field_custom(name='**Roles**', value=', '.join(self.roles), embed=embed)
if len(self.weights_roles) > 0:
weights = []
for r, n in zip(self.weights_roles, self.weights_numbers):
weights.append(f'{r}: {n}')
embed = await self.add_field_custom(name='**Weights**', value=', '.join(weights), embed=embed)
if self.roles != ['@everyone']:
embed = await self.add_field_custom(name='**Roles**', value=', '.join(self.roles), embed=embed)
if len(self.weights_roles) > 0:
weights = []
for r, n in zip(self.weights_roles, self.weights_numbers):
weights.append(f'{r}: {n}')
embed = await self.add_field_custom(name='**Weights**', value=', '.join(weights), embed=embed)
embed = await self.add_field_custom(name='**Anonymous**', value=self.anonymous, embed=embed)
# embed = await self.add_field_custom(name='**Multiple Choice**', value=self.multiple_choice, embed=embed)
embed = await self.add_field_custom(name='**Deadline**', value=await self.get_poll_status(), embed=embed)
embed = await self.add_field_custom(name='**Author**', value=self.author.name, embed=embed)
if self.duration != 0:
embed = await self.add_field_custom(name='**Deadline**', value=await self.get_poll_status(), embed=embed)
# embed = await self.add_field_custom(name='**Author**', value=self.author.name, embed=embed)
if self.reaction:
if self.options_reaction_default:
@ -976,34 +1223,47 @@ class Poll:
vote_display.append(f'{r} {self.count_votes(i)}')
embed = await self.add_field_custom(name=text, value=' '.join(vote_display), embed=embed)
else:
embed.add_field(name='\u200b', value='\u200b', inline=False)
# embed.add_field(name='\u200b', value='\u200b', inline=False)
if await self.is_open():
text = f'*Vote by adding reactions to the poll*. '
head = ""
if self.multiple_choice == 0:
text += '*You can vote for multiple options.*'
head += 'You can vote for multiple options:'
elif self.multiple_choice == 1:
text += '*You have 1 vote, but can change it.*'
head += 'You have 1 vote:'
else:
text += f'*You have {self.multiple_choice} choices and can change them.*'
head += f'You can vote for {self.multiple_choice} options:'
else:
text = f'*Final Results of the Poll* '
head = f'Final Results of the Poll '
if self.multiple_choice == 0:
text += '*(Multiple Choice).*'
head += '(Multiple Choice):'
elif self.multiple_choice == 1:
text += '*(Single Choice).*'
head += '(Single Choice):'
else:
text += f'*(With up to {self.multiple_choice} choices).*'
embed = await self.add_field_custom(name='**Options**', value=text, embed=embed)
head += f'(With up to {self.multiple_choice} choices):'
# embed = await self.add_field_custom(name='**Options**', value=text, embed=embed)
options_text = '**' + head + '**\n'
for i, r in enumerate(self.options_reaction):
embed = await self.add_field_custom(
name=f':regional_indicator_{ascii_lowercase[i]}: {self.count_votes(i)}',
value=r,
embed=embed
)
custom_icon = ''
if i in self.survey_flags:
custom_icon = '🖊'
options_text += f':regional_indicator_{ascii_lowercase[i]}:{custom_icon} {r}'
if self.hide_count and self.open:
options_text += '\n'
else:
options_text += f' **- {self.count_votes(i)} Votes**\n'
# embed = await self.add_field_custom(
# name=f':regional_indicator_{ascii_lowercase[i]}:{custom_icon} {self.count_votes(i)}',
# value=r,
# embed=embed
# )
embed.add_field(name='\u200b', value=options_text, inline=False)
# else:
# embed = await self.add_field_custom(name='**Options**', value=', '.join(self.get_options()), embed=embed)
embed.set_footer(text='React with ❔ to get info. It is not a vote option.')
custom_text = ""
if len(self.survey_flags) > 0:
custom_text = " 🖊 next to an option means you can submit a custom answer."
embed.set_footer(text='React with ❔ to get info. It is not a vote option.' + custom_text)
return embed
async def post_embed(self, destination):
@ -1129,7 +1389,7 @@ class Poll:
weight = max(valid_weights)
if str(user.id) not in self.votes:
self.votes[str(user.id)] = {'weight': weight, 'choices': []}
self.votes[str(user.id)] = {'weight': weight, 'choices': [], 'answers': []}
else:
self.votes[str(user.id)]['weight'] = weight
@ -1150,13 +1410,42 @@ class Poll:
# refresh_poll = False
else:
if self.multiple_choice > 0 and self.votes[str(user.id)]['choices'].__len__() >= self.multiple_choice:
# # auto unvote for single choice non anonymous
# if self.votes[str(user.id)]['choices'].__len__() == 1 and not self.anonymous:
# prev_choice = self.votes[str(user.id)]['choices'][0]
# if self.options_reaction_default:
# emoji = self.options_reaction[prev_choice]
# else:
# emoji = AZ_EMOJIS[prev_choice]
# await message.remove_reaction(emoji, user)
# else:
say_text = f'You have reached the **maximum choices of {self.multiple_choice}** for this poll. ' \
f'Before you can vote again, you need to unvote one of your choices.'
f'Before you can vote again, you need to unvote one of your choices.\n' \
f'Your current choices are:\n'
for c in self.votes[str(user.id)]['choices']:
if self.options_reaction_default:
say_text += f'{self.options_reaction[c]}\n'
else:
say_text += f'{AZ_EMOJIS[c]} {self.options_reaction[c]}\n'
embed = discord.Embed(title='', description=say_text, colour=SETTINGS.color)
embed.set_author(name='Pollmaster', icon_url=SETTINGS.author_icon)
await user.send(embed=embed)
# refresh_poll = False
else:
if choice in self.survey_flags:
if len(self.votes[str(user.id)]['answers']) != len(self.survey_flags):
self.votes[str(user.id)]['answers'] = ['' for i in range(len(self.survey_flags))]
custom_input = await self.ask_for_input_DM(
user,
"Custom Answer",
"For this vote option you can provide a custom reply. "
"Note that everyone will be able to see the answer. If you don't want to provide a "
"custom answer, type \"-\""
)
if not custom_input or custom_input.lower() == "-":
custom_input = "No Answer"
self.votes[str(user.id)]['answers'][self.survey_flags.index(choice)] = custom_input
self.votes[str(user.id)]['choices'].append(choice)
self.votes[str(user.id)]['choices'] = list(set(self.votes[str(user.id)]['choices']))
# else:
@ -1175,11 +1464,8 @@ class Poll:
# commit
await self.save_to_db()
asyncio.ensure_future(message.edit(embed=await self.generate_embed()))
if not self.hide_count:
asyncio.ensure_future(message.edit(embed=await self.generate_embed()))
async def unvote(self, user, option, message, lock):
if not await self.is_open():
@ -1205,9 +1491,12 @@ class Poll:
if choice != 'invalid' and choice in self.votes[str(user.id)]['choices']:
try:
if choice in self.survey_flags and len(self.votes[str(user.id)]['answers']) == len(self.survey_flags):
self.votes[str(user.id)]['answers'][self.survey_flags.index(choice)] = ''
self.votes[str(user.id)]['choices'].remove(choice)
await self.save_to_db()
asyncio.ensure_future(message.edit(embed=await self.generate_embed()))
if not self.hide_count:
asyncio.ensure_future(message.edit(embed=await self.generate_embed()))
except ValueError:
pass

View File

@ -43,7 +43,7 @@ ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
extensions = ['cogs.config', 'cogs.poll_controls', 'cogs.help', 'cogs.db_api']
extensions = ['cogs.config', 'cogs.poll_controls', 'cogs.help', 'cogs.db_api', 'cogs.admin']
for ext in extensions:
bot.load_extension(ext)
@ -56,13 +56,12 @@ async def on_ready():
bot.db = mongo.pollmaster
bot.session = aiohttp.ClientSession()
print(bot.db)
await bot.change_presence(status=discord.Game(name=f'pm!help - v2.2'))
# check discord server configs
try:
db_server_ids = [entry['_id'] async for entry in bot.db.config.find({}, {})]
for server in bot.guilds:
if server.id not in db_server_ids:
if str(server.id) not in db_server_ids:
# create new config entry
await bot.db.config.update_one(
{'_id': str(server.id)},
@ -78,11 +77,19 @@ async def on_ready():
bot.locks = {}
bot.message_cache = MessageCache(bot)
print("Servers verified. Bot running.")
game = discord.Game("Democracy 4")
await bot.change_presence(status=discord.Status.online, activity=game)
print("Servers verified. Bot running.")
@bot.event
async def on_command_error(ctx, e):
if ctx.cog.qualified_name == "Admin":
# Admin cog handles the errors locally
return
if SETTINGS.log_errors:
ignored_exceptions = (
commands.MissingRequiredArgument,
@ -96,7 +103,7 @@ async def on_command_error(ctx, e):
if isinstance(e, ignored_exceptions):
# log warnings
logger.warning(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}')
# logger.warning(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}')
return
# log error
@ -117,6 +124,7 @@ async def on_command_error(ctx, e):
# if SETTINGS.mode == 'development':
raise e
@bot.event
async def on_guild_join(server):
result = await bot.db.config.find_one({'_id': str(server.id)})
@ -128,4 +136,4 @@ async def on_guild_join(server):
)
bot.pre[str(server.id)] = 'pm!'
bot.run(SETTINGS.bot_token)
bot.run(SETTINGS.bot_token)

View File

@ -1,4 +1,4 @@
# Pollmaster V 2.3
# Pollmaster V 2.4
## Overview
@ -8,6 +8,7 @@ Here is a quick list of features:
- Voting works with reactions (users don't need to type anything)
- Anonymous voting is possible
- You can hide the current vote count to prevent sheeping
- Polls can be single choice, multiple choice or restricted to a specific number of choices
- You can prepare polls in advance and schedule them to a date and time or manually activate them
- Polls can be given a deadline or they can be open until closed manually
@ -23,19 +24,21 @@ Here is a quick list of features:
Here is how Pollmaster looks in action:
![Pollmaster in action](https://i.imgur.com/vSXgd0r.png "Poll 1")
![Pollmaster in action](https://i.imgur.com/C3zqnK2.png "Poll 1")
![Pollmaster in action](https://i.imgur.com/kMbnmiJ.png "Poll 2")
![Pollmaster in action](https://i.imgur.com/an0E3EO.png "Poll 2")
## The most important commands
| Command | Effect |
|------------------------|----------------------------------------------------|
| pm!help | Shows an interactive help menu |
| pm!new | Starts a new poll with all the settings |
| pm!new | Starts a new poll with the most common settings |
| pm!advanced | Starts a new poll 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 |
| @mention prefix | Show the prefix if you forgot it |
| pm!userrole <any role> | Set the role that has the rights to use the bot |
## Getting Started