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__/ essentials/__pycache__/
utils/__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': if SETTINGS.mode == 'production':
await self.dblpy.post_server_count() await self.dblpy.post_server_count()
logger.info('posted server count ({})'.format(len(self.bot.guilds))) 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: except Exception as e:
logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e)) logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e))
await asyncio.sleep(1800) await asyncio.sleep(1800)

View File

@ -12,11 +12,10 @@ class Help(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.pages = ['🏠', '🆕', '🔍', '🕹', '🛠', '💖'] self.pages = ['🏠', '🆕', '🔍', '🕹', '🛠', '', '💖']
async def embed_list_reaction_handler(self, ctx, page, pre, msg=None): async def embed_list_reaction_handler(self, ctx, page, pre, msg=None):
embed = self.get_help_embed(page, pre) embed = self.get_help_embed(page, pre)
if msg is None: if msg is None:
msg = await ctx.send(embed=embed) msg = await ctx.send(embed=embed)
# add reactions # add reactions
@ -39,8 +38,6 @@ class Help(commands.Cog):
await reaction.message.remove_reaction(reaction.emoji, user) await reaction.message.remove_reaction(reaction.emoji, user)
return reaction return reaction
def get_help_embed(self, page, pre): def get_help_embed(self, page, pre):
title = f' Pollmaster Help - React with an emoji to learn more about a topic!' 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.') embed.set_footer(text='Use reactions to navigate the help. This message will self-destruct in 3 minutes.')
if page == '🏠': if page == '🏠':
## POLL CREATION SHORT # POLL CREATION SHORT
embed.add_field(name='🆕 Making New Polls', 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='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='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?`', # 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 ## POLL CONTROLS
embed.add_field(name='🔍 Show Polls', 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='Command', value=f'`{pre}show (label)`', inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: `open` (default) | `closed` | `prepared` | ' # embed.add_field(name='Arguments', value=f'Arguments: `open` (default) | `closed` | `prepared` | '
# f'`<poll_label>` (optional)', inline=False) # f'`<poll_label>` (optional)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}show` | `{pre}show closed` | `{pre}show mascot`', # embed.add_field(name='Examples', value=f'Examples: `{pre}show` | `{pre}show closed` | `{pre}show mascot`',
# inline=False) # inline=False)
## POLL CONTROLS # POLL CONTROLS
embed.add_field(name='🕹 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` ', # embed.add_field(name='Commands', value=f'`{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ',
# inline=False) # inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: <poll_label> (required)', 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`', # embed.add_field(name='Examples', value=f'Examples: `{pre}close mascot` | `{pre}export proposal`',
# inline=False) # inline=False)
## POLL CONTROLS # POLL CONTROLS
embed.add_field(name='🛠 Configuration', 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 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', 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.', value='More infos about Pollmaster, the developer, where to go for further help and how you can support us.',
inline=False) inline=False)
elif page == '🆕': elif page == '🆕':
embed.add_field(name='🆕 Making New Polls', 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 ' 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 that first step.' '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 in 🛠 Configuration.', 'Your Members need the <admin> or <user> role to use these commands. '
'More on user rights in 🛠 Configuration.',
inline=False) 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 ' 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.', 'question and your answers; the rest will be set to default values.',
inline=False) inline=False)
embed.add_field(name=f'🔹 **All Features:** `{pre}new <poll question>` (optional)', embed.add_field(name=f'🔹 **Basic Poll:** `{pre}new`',
value='This command gives you full control over your poll. A step by step wizard will guide ' 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, ' '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) inline=False)
embed.add_field(name=f'🔹 **Prepare and Schedule:** `{pre}prepare <poll question>` (optional)', embed.add_field(name=f'🔹 **Advanced Poll:** `{pre}advanced`',
value=f'Similar to `{pre}new`, this gives you all the options. But additionally, the poll will ' 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 ' '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. ' f'and/or if you would like to manually `{pre}activate` it. '
'Perfect if you are preparing for a team meeting!', 'Perfect if you are preparing for a team meeting!',
inline=False) 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' 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'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'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'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) inline=False)
elif page == '🔍': elif page == '🔍':
@ -137,9 +144,15 @@ class Help(commands.Cog):
inline=False) inline=False)
elif page == '🕹': elif page == '🕹':
embed.add_field(name='🕹 Poll Controls', 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.', 'Go to 🛠 Configuration for more info on the permissions.',
inline=False) 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>`', embed.add_field(name=f'🔹 **Close** `{pre}close <poll_label>`',
value='Polls will close automatically when their deadline is reached. But you can always ' 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 ' '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).', 'whitespace, use "\w" instead of " " (discord deletes trailing whitespaces).',
inline=False) 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 == '💖': elif page == '💖':
embed.add_field(name='💖 Pollmaster 💖', embed.add_field(name='💖 Pollmaster 💖',
value='If you enjoy the bot, you can show your appreciation by giving him an upvote on Discordbots.', 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 return embed
@commands.command() @commands.command()
async def help(self, ctx, *, topic=None): async def help(self, ctx):
server = await ask_for_server(self.bot, ctx.message) 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) pre = await get_server_pre(self.bot, server)
rct = 1 rct = 1
while rct is not None: while rct is not None:
@ -217,9 +251,89 @@ class Help(commands.Cog):
page = rct.emoji page = rct.emoji
msg = rct.message msg = rct.message
rct = await self.embed_list_reaction_handler(ctx, page, pre, msg) rct = await self.embed_list_reaction_handler(ctx, page, pre, msg)
# print(res.user, res.reaction, res.reaction.emoji)
# cleanup # 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): def setup(bot):

View File

@ -11,26 +11,27 @@ import pytz
from discord.ext import commands from discord.ext import commands
from utils.misc import CustomFormatter from utils.misc import CustomFormatter
from .poll import Poll from models.poll import Poll
from utils.paginator import embed_list_paginated from utils.paginator import embed_list_paginated
from essentials.multi_server import get_server_pre, ask_for_server, ask_for_channel from essentials.multi_server import get_server_pre, ask_for_server, ask_for_channel
from essentials.settings import SETTINGS 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 # 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 AZ_EMOJIS = [(b'\\U0001f1a'.replace(b'a', bytes(hex(224 + (6 + i))[2:], "utf-8"))).decode("unicode-escape") for i in
range(26)] range(26)]
class PollControls(commands.Cog): class PollControls(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = 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 = {} self.ignore_next_removed_reaction = {}
#
# # General Methods # # General Methods
def get_label(self, message : discord.Message): def get_label(self, message: discord.Message):
label = None label = None
if message and message.embeds: if message and message.embeds:
embed = message.embeds[0] 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""" """This function runs every 60 seconds to schedule prepared polls and close expired polls"""
while True: while True:
try: 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")}}) query = self.bot.db.polls.find({'active': False, 'activation': {"$not": re.compile("0")}})
if query: if query:
for pd in [poll async for poll in query]: for pd in [poll async for poll in query]:
@ -73,17 +78,17 @@ class PollControls(commands.Cog):
except: except:
continue continue
except AttributeError as ae: except AttributeError as ae:
#Database not loaded yet # Database not loaded yet
logger.warning("Attribute Error in close_polls loop") logger.warning("Attribute Error in close_polls loop")
logger.exception(ae) logger.exception(ae)
pass pass
except Exception as ex: 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.error("Other Error in close_polls loop")
logger.exception(ex) logger.exception(ex)
pass pass
await asyncio.sleep(30) await asyncio.sleep(60)
def get_lock(self, server_id): def get_lock(self, server_id):
if not self.bot.locks.get(server_id): if not self.bot.locks.get(server_id):
@ -119,7 +124,7 @@ class PollControls(commands.Cog):
embed.set_footer(text=footer_text) embed.set_footer(text=footer_text)
await ctx.send(embed=embed) await ctx.send(embed=embed)
# # Commands # Commands
@commands.command() @commands.command()
async def activate(self, ctx, *, short=None): async def activate(self, ctx, *, short=None):
"""Activate a prepared poll. Parameter: <label>""" """Activate a prepared poll. Parameter: <label>"""
@ -229,6 +234,31 @@ class PollControls(commands.Cog):
await self.say_error(ctx, error) await self.say_error(ctx, error)
await ctx.invoke(self.show) 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() @commands.command()
async def export(self, ctx, *, short=None): async def export(self, ctx, *, short=None):
'''Export a poll. Parameter: <label>''' '''Export a poll. Parameter: <label>'''
@ -317,87 +347,96 @@ class PollControls(commands.Cog):
pre = await get_server_pre(self.bot, server) pre = await get_server_pre(self.bot, server)
footer = f'Type {pre}show to display all polls' footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error, footer) await self.say_error(ctx, error, footer)
#
# @commands.command() @commands.command()
# 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)
# # if not server: server = await ask_for_server(self.bot, ctx.message)
# # return if not server:
# # pre = await get_server_pre(self.bot, server) return
# # try: pre = await get_server_pre(self.bot, server)
# # # generate the argparser and handle invalid stuff try:
# # descr = 'Accept poll settings via commandstring. \n\n' \ # generate the argparser and handle invalid stuff
# # '**Wrap all arguments in quotes like this:** \n' \ descr = 'Accept poll settings via commandstring. \n\n' \
# # f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \ '**Wrap all arguments in quotes like this:** \n' \
# # 'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \ f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \
# # 'If an argument is invalid, the wizard will step in. ' \ 'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \
# # 'If the command string is invalid, you will get this error :)' 'If an argument is invalid, the wizard will step in. ' \
# # parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False) 'If the command string is invalid, you will get this error :)'
# # parser.add_argument('-question', '-q') parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False)
# # parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id))) parser.add_argument('-question', '-q')
# # parser.add_argument('-options', '-o') parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id)))
# # parser.add_argument('-multiple_choice', '-mc', default='1') parser.add_argument('-anonymous', '-a', action="store_true")
# # parser.add_argument('-roles', '-r', default='all') parser.add_argument('-options', '-o')
# # parser.add_argument('-weights', '-w', default='none') parser.add_argument('-survey_flags', '-sf', default='0')
# # parser.add_argument('-deadline', '-d', default='0') parser.add_argument('-multiple_choice', '-mc', default='1')
# # parser.add_argument('-anonymous', '-a', action="store_true") parser.add_argument('-hide_votes', '-h', action="store_true")
# # parser.add_argument('-roles', '-r', default='all')
# # helpstring = parser.format_help() parser.add_argument('-weights', '-w', default='none')
# # helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ") parser.add_argument('-prepare', '-p', default='-1')
# # parser.add_argument('-deadline', '-d', default='0')
# # if cmd and cmd == 'help':
# # await self.say_embed(ctx, say_text=helpstring) helpstring = parser.format_help()
# # return helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
# #
# # try: if not cmd or len(cmd) < 2 or cmd == 'help':
# # cmds = shlex.split(cmd) # Shlex will block if the string is empty
# # except ValueError: await self.say_embed(ctx, say_text=helpstring)
# # await self.say_error(ctx, error_text=helpstring) return
# # return
# # except: try:
# # return cmds = shlex.split(cmd)
# # except ValueError:
# # try: await self.say_error(ctx, error_text=helpstring)
# # args, unknown_args = parser.parse_known_args(cmds) return
# # except SystemExit: except:
# # await self.say_error(ctx, error_text=helpstring) return
# # return
# # except: try:
# # return args, unknown_args = parser.parse_known_args(cmds)
# # except SystemExit:
# # if unknown_args: await self.say_error(ctx, error_text=helpstring)
# # error_text = f'**There was an error reading the command line options!**.\n' \ return
# # f'Most likely this is because you didn\'t surround the arguments with double quotes like this: ' \ except:
# # f'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \ return
# # f'\n\nHere are the arguments I could not understand:\n'
# # error_text += '`'+'\n'.join(unknown_args)+'`' if unknown_args:
# # error_text += f'\n\nHere are the arguments which are ok:\n' error_text = f'**There was an error reading the command line options!**.\n' \
# # error_text += '`' + '\n'.join([f'{k}: {v}' for k, v in vars(args).items()]) + '`' 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"`' \
# # await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.') f'\n\nHere are the arguments I could not understand:\n'
# # return error_text += '`'+'\n'.join(unknown_args)+'`'
# # error_text += f'\n\nHere are the arguments which are ok:\n'
# # # pass arguments to the wizard error_text += '`' + '\n'.join([f'{k}: {v}' for k, v in vars(args).items()]) + '`'
# # async def route(poll):
# # await poll.set_name(force=args.question) await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.')
# # await poll.set_short(force=args.label) return
# # await poll.set_anonymous(force=f'{"yes" if args.anonymous else "no"}')
# # await poll.set_options_reaction(force=args.options) # pass arguments to the wizard
# # await poll.set_multiple_choice(force=args.multiple_choice) async def route(poll):
# # await poll.set_roles(force=args.roles) await poll.set_name(ctx, force=args.question)
# # await poll.set_weights(force=args.weights) await poll.set_short(ctx, force=args.label)
# # await poll.set_duration(force=args.deadline) await poll.set_anonymous(ctx, force=f'{"yes" if args.anonymous else "no"}')
# # await poll.set_options_reaction(ctx, force=args.options)
# # poll = await self.wizard(ctx, route, server) await poll.set_survey_flags(ctx, force=args.survey_flags)
# # if poll: await poll.set_multiple_choice(ctx, force=args.multiple_choice)
# # await poll.post_embed(destination=poll.channel) await poll.set_hide_vote_count(ctx, force=f'{"yes" if args.hide_votes else "no"}')
# # except Exception as error: await poll.set_roles(ctx, force=args.roles)
# # logger.error("ERROR IN pm!cmd") await poll.set_weights(ctx, force=args.weights)
# # logger.exception(error) 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() @commands.command()
async def quick(self, ctx, *, cmd=None): async def quick(self, ctx, *, cmd=None):
'''Create a quick poll with just a question and some options. Parameters: <Question> (optional)''' '''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_anonymous(ctx, force='no')
await poll.set_options_reaction(ctx) await poll.set_options_reaction(ctx)
await poll.set_multiple_choice(ctx, force='1') 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_roles(ctx, force='all')
await poll.set_weights(ctx, force='none') await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx, force='0') await poll.set_duration(ctx, force='0')
@ -432,7 +472,9 @@ class PollControls(commands.Cog):
await poll.set_preparation(ctx) await poll.set_preparation(ctx)
await poll.set_anonymous(ctx) await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx) await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx)
await poll.set_multiple_choice(ctx) await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx)
await poll.set_roles(ctx) await poll.set_roles(ctx)
await poll.set_weights(ctx) await poll.set_weights(ctx)
await poll.set_duration(ctx) await poll.set_duration(ctx)
@ -441,6 +483,29 @@ class PollControls(commands.Cog):
if poll: if poll:
await poll.post_embed(ctx.message.author) 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() @commands.command()
async def new(self, ctx, *, cmd=None): async def new(self, ctx, *, cmd=None):
"""Start the poll wizard to create a new poll step by step. Parameters: <Question> (optional)""" """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_short(ctx)
await poll.set_anonymous(ctx) await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx) await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx, force='0')
await poll.set_multiple_choice(ctx) await poll.set_multiple_choice(ctx)
await poll.set_roles(ctx) await poll.set_hide_vote_count(ctx, force='no')
await poll.set_weights(ctx) await poll.set_roles(ctx, force='all')
await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx) await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server) poll = await self.wizard(ctx, route, server)
@ -468,13 +535,14 @@ class PollControls(commands.Cog):
if not channel: if not channel:
return return
pre = await get_server_pre(self.bot, server)
# Permission Check # Permission Check
member = server.get_member(ctx.message.author.id) member = server.get_member(ctx.message.author.id)
if not member.guild_permissions.manage_guild: if not member.guild_permissions.manage_guild:
result = await self.bot.db.config.find_one({'_id': str(server.id)}) 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( 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]: '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. ' 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. ' '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 ' 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) poll = Poll(self.bot, ctx, server, channel)
## Route to define object, passed as argument for different constructors ## 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: try:
await route(poll) await route(poll)
poll.finalize() poll.finalize()
await poll.clean_up(ctx.channel)
except StopWizard: except StopWizard:
await poll.clean_up(ctx.channel)
return return
# Finalize # Finalize
await poll.save_to_db() await poll.save_to_db()
return poll 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() @commands.Cog.listener()
async def on_raw_reaction_remove(self, data): async def on_raw_reaction_remove(self, data):
# get emoji symbol # get emoji symbol
@ -672,6 +725,10 @@ class PollControls(commands.Cog):
description='', color=SETTINGS.color) description='', color=SETTINGS.color)
embed.set_author(name=f" >> {p.short}", icon_url=SETTINGS.author_icon) 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
vote_rights = await p.has_required_role(member) vote_rights = await p.has_required_role(member)
embed.add_field(name=f'{"Can you vote?" if is_open else "Could you vote?"}', 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
edit_rights = False edit_rights = False
if str(member.id) == str(p.author): if str(member.id) == str(p.author.id):
edit_rights = True edit_rights = True
elif member.guild_permissions.manage_guild: elif member.guild_permissions.manage_guild:
edit_rights = True edit_rights = True
@ -724,7 +781,7 @@ class PollControls(commands.Cog):
await user.send(embed=embed) await user.send(embed=embed)
# send current details of who currently voted for what # 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' \ msg = '--------------------------------------------\n' \
'CURRENT VOTES\n' \ 'CURRENT VOTES\n' \
'--------------------------------------------\n' '--------------------------------------------\n'
@ -734,17 +791,21 @@ class PollControls(commands.Cog):
msg += "**" +o+":**" msg += "**" +o+":**"
c = 0 c = 0
for user_id in p.votes: 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']: if not member or i not in p.votes[str(user_id)]['choices']:
continue continue
c += 1 c += 1
name = member.nick name = member.nick
if not name: if not name:
name = member.name name = member.name
if not name:
name = "<Deleted User>"
msg += f'\n{name}' msg += f'\n{name}'
if p.votes[str(user_id)]['weight'] != 1: if p.votes[str(user_id)]['weight'] != 1:
msg += f' (weight: {p.votes[str(user_id)]["weight"]})' 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']]) # 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: if msg.__len__() > 1500:
await user.send(msg) await user.send(msg)
msg = '' msg = ''
@ -754,6 +815,29 @@ class PollControls(commands.Cog):
if msg.__len__() > 0: if msg.__len__() > 0:
await user.send(msg) 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 return
# Assume: User wants to vote with reaction # 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 self.ignore_next_removed_reaction[str(message.id) + str(emoji)] = user_id
asyncio.ensure_future(message.remove_reaction(emoji, user)) 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 # order here is crucial since we can't determine if a reaction was removed by the bot or user
# update database with vote # update database with vote
await p.vote(member, emoji, message, lock) await p.vote(member, emoji, message, lock)
@ -785,6 +868,7 @@ class PollControls(commands.Cog):
# await message.remove_reaction(r.emoji, user) # await message.remove_reaction(r.emoji, user)
# pass # pass
def setup(bot): def setup(bot):
global logger global logger
logger = logging.getLogger('bot') logger = logging.getLogger('bot')

View File

@ -21,7 +21,7 @@ async def get_pre(bot, message):
async def get_server_pre(bot, server): async def get_server_pre(bot, server):
'''Gets the prefix for a server.''' """Gets the prefix for a server."""
try: try:
#result = await bot.db.config.find_one({'_id': str(server.id)}) #result = await bot.db.config.find_one({'_id': str(server.id)})
result = bot.pre[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): 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: if message.guild is None:
list_of_shared_servers = [] list_of_shared_servers = []
for s in bot.guilds: for s in bot.guilds:
@ -148,7 +148,7 @@ async def ask_for_channel(ctx, bot, server, message):
def check(m): def check(m):
return message.author.id == m.author.id return message.author.id == m.author.id
try: try:
reply = await bot.wait_for('message', timeout=60) reply = await bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
else: else:

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 datetime
import logging import logging
import os import os
import re import random
import dateparser import dateparser
import pytz import pytz
import regex import regex
@ -23,13 +24,13 @@ from utils.misc import possible_timezones
logger = logging.getLogger('bot') logger = logging.getLogger('bot')
## Helvetica is the closest font to Whitney (discord uses Whitney) in afm # 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 # 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') afm_fname = os.path.join(rcParams['datapath'], 'fonts', 'afm', 'phvr8a.afm')
with open(afm_fname, 'rb') as fh: with open(afm_fname, 'rb') as fh:
afm = AFM(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 AZ_EMOJIS = [(b'\\U0001f1a'.replace(b'a', bytes(hex(224 + (6 + i))[2:], "utf-8"))).decode("unicode-escape") for i in
range(26)] range(26)]
@ -57,10 +58,12 @@ class Poll:
self.name = "Quick Poll" self.name = "Quick Poll"
self.short = str(uuid4())[0:23] self.short = str(uuid4())[0:23]
self.anonymous = False self.anonymous = False
self.hide_count = False
self.reaction = True self.reaction = True
self.multiple_choice = 1 self.multiple_choice = 1
self.options_reaction = ['yes', 'no'] self.options_reaction = ['yes', 'no']
self.options_reaction_default = False self.options_reaction_default = False
self.survey_flags = []
# self.options_traditional = [] # self.options_traditional = []
# self.options_traditional_default = False # self.options_traditional_default = False
self.roles = ['@everyone'] self.roles = ['@everyone']
@ -76,6 +79,18 @@ class Poll:
self.activation_tz = 0.0 self.activation_tz = 0.0
self.votes = {} 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): async def is_open(self, update_db=True):
if self.server is None: if self.server is None:
self.open = True self.open = True
@ -102,7 +117,9 @@ class Poll:
embed = discord.Embed(title="Poll creation Wizard", description=text, color=SETTINGS.color) embed = discord.Embed(title="Poll creation Wizard", description=text, color=SETTINGS.color)
if footer: if footer:
embed.set_footer(text="Type `stop` to cancel the wizard.") 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): async def wizard_says_edit(self, message, text, add=False):
if add and message.embeds.__len__() > 0: if add and message.embeds.__len__() > 0:
@ -127,9 +144,13 @@ class Poll:
"""Pre-parse user input for wizard""" """Pre-parse user input for wizard"""
def check(m): def check(m):
return m.author == self.author 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: if reply and reply.content:
self.wizard_messages.append(reply)
if reply.content.startswith(await get_pre(self.bot, 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' 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}`', f'Stopping the Wizard and then executing the command:\n`{reply.content}`',
@ -144,7 +165,8 @@ class Poll:
else: else:
raise InvalidInput raise InvalidInput
def sanitize_string(self, string): @staticmethod
def sanitize_string(string):
"""Sanitize user input for wizard""" """Sanitize user input for wizard"""
# sanitize input # sanitize input
if string is None: if string is None:
@ -239,7 +261,6 @@ class Poll:
await self.add_error(message, await self.add_error(message,
f'**The label `{reply}` is not unique on this server. Choose a different one!**') f'**The label `{reply}` is not unique on this server. Choose a different one!**')
async def set_preparation(self, ctx, force=None): async def set_preparation(self, ctx, force=None):
"""Set the preparation conditions for the Poll.""" """Set the preparation conditions for the Poll."""
async def get_valid(in_reply): async def get_valid(in_reply):
@ -269,6 +290,9 @@ class Poll:
raise DateOutOfRange(dt) raise DateOutOfRange(dt)
return dt return dt
if str(force) == '-1':
return
try: try:
dt = await get_valid(force) dt = await get_valid(force)
self.activation = dt self.activation = dt
@ -302,7 +326,7 @@ class Poll:
self.active = False self.active = False
break break
except InvalidInput: 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: except TypeError:
await self.add_error(message, '**Type Error.**') await self.add_error(message, '**Type Error.**')
except DateOutOfRange as e: except DateOutOfRange as e:
@ -404,7 +428,6 @@ class Poll:
except OutOfRange: except OutOfRange:
await self.add_error(message, '**You can\'t have more choices than options.**') await self.add_error(message, '**You can\'t have more choices than options.**')
async def set_options_reaction(self, ctx, force=None): async def set_options_reaction(self, ctx, force=None):
"""Set the answers / options of the Poll.""" """Set the answers / options of the Poll."""
async def get_valid(in_reply): async def get_valid(in_reply):
@ -418,7 +441,7 @@ class Poll:
return int(split[0]) return int(split[0])
else: else:
raise WrongNumberOfArguments raise WrongNumberOfArguments
elif 1 < split.__len__() <= 26: elif 1 < split.__len__() <= 18:
split = [self.sanitize_string(o) for o in split] split = [self.sanitize_string(o) for o in split]
if any([len(o) < 1 for o in split]): if any([len(o) < 1 for o in split]):
raise InvalidInput raise InvalidInput
@ -427,22 +450,11 @@ class Poll:
else: else:
raise WrongNumberOfArguments 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: try:
options = await get_valid(force) options = await get_valid(force)
self.options_reaction_default = False self.options_reaction_default = False
if isinstance(options, int): if isinstance(options, int):
self.options_reaction = get_preset_options(options) self.options_reaction = self.get_preset_options(options)
if options <= 3: if options <= 3:
self.options_reaction_default = True self.options_reaction_default = True
else: else:
@ -473,7 +485,7 @@ class Poll:
options = await get_valid(reply) options = await get_valid(reply)
self.options_reaction_default = False self.options_reaction_default = False
if isinstance(options, int): if isinstance(options, int):
self.options_reaction = get_preset_options(options) self.options_reaction = self.get_preset_options(options)
if options <= 3: if options <= 3:
self.options_reaction_default = True self.options_reaction_default = True
else: else:
@ -483,11 +495,115 @@ class Poll:
except InvalidInput: except InvalidInput:
await self.add_error(message, await self.add_error(message,
'**Invalid entry. Type `1`, `2`, `3` or `4` or a comma separated list of ' '**Invalid entry. Type `1`, `2`, `3` or `4` or a comma separated list of '
'up to 26 options.**') 'up to 18 options.**')
except WrongNumberOfArguments: except WrongNumberOfArguments:
await self.add_error(message, 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): async def set_roles(self, ctx, force=None):
"""Set role restrictions for the Poll.""" """Set role restrictions for the Poll."""
@ -500,7 +616,7 @@ class Poll:
if split.__len__() == 1 and split[0] in ['0', 'all', 'everyone']: if split.__len__() == 1 and split[0] in ['0', 'all', 'everyone']:
return ['@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]): if not all([r.isdigit() for r in split]):
raise ExpectedInteger raise ExpectedInteger
elif any([int(r) > n_roles for r in split]): elif any([int(r) > n_roles for r in split]):
@ -614,6 +730,7 @@ class Poll:
force = None force = None
else: else:
reply = await self.get_user_reply(ctx) reply = await self.get_user_reply(ctx)
print(reply)
w_n = await get_valid(reply, self.server.roles) w_n = await get_valid(reply, self.server.roles)
self.weights_roles = w_n[0] self.weights_roles = w_n[0]
self.weights_numbers = w_n[1] 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')) await self.add_vaild(message, self.duration.strftime('%d-%b-%Y %H:%M %Z'))
break break
except InvalidInput: 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: except TypeError:
await self.add_error(message, '**Type Error.**') await self.add_error(message, '**Type Error.**')
except DateOutOfRange as e: except DateOutOfRange as e:
@ -699,6 +816,85 @@ class Poll:
def finalize(self): def finalize(self):
self.time_created = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) 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): async def to_dict(self):
if self.channel is None: if self.channel is None:
cid = 0 cid = 0
@ -715,11 +911,13 @@ class Poll:
'name': self.name, 'name': self.name,
'short': self.short, 'short': self.short,
'anonymous': self.anonymous, 'anonymous': self.anonymous,
'hide_count': self.hide_count,
'reaction': self.reaction, 'reaction': self.reaction,
'multiple_choice': self.multiple_choice, 'multiple_choice': self.multiple_choice,
'options_reaction': self.options_reaction, 'options_reaction': self.options_reaction,
'reaction_default': self.options_reaction_default, 'reaction_default': self.options_reaction_default,
#'options_traditional': self.options_traditional, #'options_traditional': self.options_traditional,
'survey_flags': self.survey_flags,
'roles': self.roles, 'roles': self.roles,
'weights_roles': self.weights_roles, 'weights_roles': self.weights_roles,
'weights_numbers': self.weights_numbers, 'weights_numbers': self.weights_numbers,
@ -766,7 +964,7 @@ class Poll:
f'Question / Name: {self.name}\n' f'Question / Name: {self.name}\n'
f'Label: {self.short}\n' f'Label: {self.short}\n'
f'Anonymous: {"Yes" if self.anonymous else "No"}\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'Answer options: {", ".join(self.options_reaction)}\n'
f'Allowed roles: {", ".join(self.roles) if self.roles.__len__() > 0 else "@everyone"}\n' f'Allowed roles: {", ".join(self.roles) if self.roles.__len__() > 0 else "@everyone"}\n'
f'Weights for roles: {weight_str}\n' f'Weights for roles: {weight_str}\n'
@ -795,10 +993,21 @@ class Poll:
if not name: if not name:
name = member.name name = member.name
export += f'\n{name}' export += f'\n{name}: '
if self.votes[str(user_id)]['weight'] != 1: if self.votes[str(user_id)]['weight'] != 1:
export += f' (weight: {self.votes[str(user_id)]["weight"]})' 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' export += '\n'
else: else:
export += '--------------------------------------------\n' \ export += '--------------------------------------------\n' \
@ -821,6 +1030,28 @@ class Poll:
# 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']])
export += '\n' 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' export += ('--------------------------------------------\n'
'BOT DETAILS\n' 'BOT DETAILS\n'
'--------------------------------------------\n' '--------------------------------------------\n'
@ -854,6 +1085,13 @@ class Poll:
self.name = d['name'] self.name = d['name']
self.short = d['short'] self.short = d['short']
self.anonymous = d['anonymous'] 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'] self.reaction = d['reaction']
# backwards compatibility for multiple choice # backwards compatibility for multiple choice
@ -872,6 +1110,13 @@ class Poll:
self.options_reaction = d['options_reaction'] self.options_reaction = d['options_reaction']
self.options_reaction_default = d['reaction_default'] self.options_reaction_default = d['reaction_default']
# self.options_traditional = d['options_traditional'] # 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.roles = d['roles']
self.weights_roles = d['weights_roles'] self.weights_roles = d['weights_roles']
self.weights_numbers = d['weights_numbers'] 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='**Poll Question**', value=self.name, embed=embed)
embed = await self.add_field_custom(name='**Roles**', value=', '.join(self.roles), embed=embed) if self.roles != ['@everyone']:
if len(self.weights_roles) > 0: embed = await self.add_field_custom(name='**Roles**', value=', '.join(self.roles), embed=embed)
weights = [] if len(self.weights_roles) > 0:
for r, n in zip(self.weights_roles, self.weights_numbers): weights = []
weights.append(f'{r}: {n}') for r, n in zip(self.weights_roles, self.weights_numbers):
embed = await self.add_field_custom(name='**Weights**', value=', '.join(weights), embed=embed) 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='**Anonymous**', value=self.anonymous, embed=embed)
# embed = await self.add_field_custom(name='**Multiple Choice**', value=self.multiple_choice, 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='**Deadline**', value=await self.get_poll_status(), embed=embed)
embed = await self.add_field_custom(name='**Author**', value=self.author.name, embed=embed)
# embed = await self.add_field_custom(name='**Author**', value=self.author.name, embed=embed)
if self.reaction: if self.reaction:
if self.options_reaction_default: if self.options_reaction_default:
@ -976,34 +1223,47 @@ class Poll:
vote_display.append(f'{r} {self.count_votes(i)}') vote_display.append(f'{r} {self.count_votes(i)}')
embed = await self.add_field_custom(name=text, value=' '.join(vote_display), embed=embed) embed = await self.add_field_custom(name=text, value=' '.join(vote_display), embed=embed)
else: else:
embed.add_field(name='\u200b', value='\u200b', inline=False) # embed.add_field(name='\u200b', value='\u200b', inline=False)
if await self.is_open(): if await self.is_open():
text = f'*Vote by adding reactions to the poll*. ' head = ""
if self.multiple_choice == 0: if self.multiple_choice == 0:
text += '*You can vote for multiple options.*' head += 'You can vote for multiple options:'
elif self.multiple_choice == 1: elif self.multiple_choice == 1:
text += '*You have 1 vote, but can change it.*' head += 'You have 1 vote:'
else: else:
text += f'*You have {self.multiple_choice} choices and can change them.*' head += f'You can vote for {self.multiple_choice} options:'
else: else:
text = f'*Final Results of the Poll* ' head = f'Final Results of the Poll '
if self.multiple_choice == 0: if self.multiple_choice == 0:
text += '*(Multiple Choice).*' head += '(Multiple Choice):'
elif self.multiple_choice == 1: elif self.multiple_choice == 1:
text += '*(Single Choice).*' head += '(Single Choice):'
else: else:
text += f'*(With up to {self.multiple_choice} choices).*' head += f'(With up to {self.multiple_choice} choices):'
embed = await self.add_field_custom(name='**Options**', value=text, embed=embed) # embed = await self.add_field_custom(name='**Options**', value=text, embed=embed)
options_text = '**' + head + '**\n'
for i, r in enumerate(self.options_reaction): for i, r in enumerate(self.options_reaction):
embed = await self.add_field_custom( custom_icon = ''
name=f':regional_indicator_{ascii_lowercase[i]}: {self.count_votes(i)}', if i in self.survey_flags:
value=r, custom_icon = '🖊'
embed=embed 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: # else:
# 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)
custom_text = ""
embed.set_footer(text='React with ❔ to get info. It is not a vote option.') 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 return embed
async def post_embed(self, destination): async def post_embed(self, destination):
@ -1129,7 +1389,7 @@ class Poll:
weight = max(valid_weights) weight = max(valid_weights)
if str(user.id) not in self.votes: 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: else:
self.votes[str(user.id)]['weight'] = weight self.votes[str(user.id)]['weight'] = weight
@ -1150,13 +1410,42 @@ class Poll:
# refresh_poll = False # refresh_poll = False
else: else:
if self.multiple_choice > 0 and self.votes[str(user.id)]['choices'].__len__() >= self.multiple_choice: 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. ' \ 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 = discord.Embed(title='', description=say_text, colour=SETTINGS.color)
embed.set_author(name='Pollmaster', icon_url=SETTINGS.author_icon) embed.set_author(name='Pollmaster', icon_url=SETTINGS.author_icon)
await user.send(embed=embed) await user.send(embed=embed)
# refresh_poll = False # refresh_poll = False
else: 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'].append(choice)
self.votes[str(user.id)]['choices'] = list(set(self.votes[str(user.id)]['choices'])) self.votes[str(user.id)]['choices'] = list(set(self.votes[str(user.id)]['choices']))
# else: # else:
@ -1175,11 +1464,8 @@ class Poll:
# commit # commit
await self.save_to_db() 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): async def unvote(self, user, option, message, lock):
if not await self.is_open(): if not await self.is_open():
@ -1205,9 +1491,12 @@ class Poll:
if choice != 'invalid' and choice in self.votes[str(user.id)]['choices']: if choice != 'invalid' and choice in self.votes[str(user.id)]['choices']:
try: 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) self.votes[str(user.id)]['choices'].remove(choice)
await self.save_to_db() 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: except ValueError:
pass pass

View File

@ -43,7 +43,7 @@ ch.setFormatter(formatter)
logger.addHandler(fh) logger.addHandler(fh)
logger.addHandler(ch) 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: for ext in extensions:
bot.load_extension(ext) bot.load_extension(ext)
@ -56,13 +56,12 @@ 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(status=discord.Game(name=f'pm!help - v2.2'))
# check discord server configs # check discord server configs
try: try:
db_server_ids = [entry['_id'] async for entry in bot.db.config.find({}, {})] db_server_ids = [entry['_id'] async for entry in bot.db.config.find({}, {})]
for server in bot.guilds: 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 # create new config entry
await bot.db.config.update_one( await bot.db.config.update_one(
{'_id': str(server.id)}, {'_id': str(server.id)},
@ -78,11 +77,19 @@ async def on_ready():
bot.locks = {} bot.locks = {}
bot.message_cache = MessageCache(bot) 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 @bot.event
async def on_command_error(ctx, e): async def on_command_error(ctx, e):
if ctx.cog.qualified_name == "Admin":
# Admin cog handles the errors locally
return
if SETTINGS.log_errors: if SETTINGS.log_errors:
ignored_exceptions = ( ignored_exceptions = (
commands.MissingRequiredArgument, commands.MissingRequiredArgument,
@ -96,7 +103,7 @@ async def on_command_error(ctx, e):
if isinstance(e, ignored_exceptions): if isinstance(e, ignored_exceptions):
# log warnings # 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 return
# log error # log error
@ -117,6 +124,7 @@ async def on_command_error(ctx, e):
# if SETTINGS.mode == 'development': # if SETTINGS.mode == 'development':
raise e raise e
@bot.event @bot.event
async def on_guild_join(server): async def on_guild_join(server):
result = await bot.db.config.find_one({'_id': str(server.id)}) result = await bot.db.config.find_one({'_id': str(server.id)})

View File

@ -1,4 +1,4 @@
# Pollmaster V 2.3 # Pollmaster V 2.4
## Overview ## Overview
@ -8,6 +8,7 @@ Here is a quick list of features:
- Voting works with reactions (users don't need to type anything) - Voting works with reactions (users don't need to type anything)
- Anonymous voting is possible - 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 - 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 - 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 - 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: 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 ## The most important commands
| Command | Effect | | Command | Effect |
|------------------------|----------------------------------------------------| |------------------------|----------------------------------------------------|
| 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 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!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!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 |
| @mention prefix | Show the prefix if you forgot it |
| 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 |
## Getting Started ## Getting Started