From 3df015d132e9e00662ef9e13316bcf4b6b91ca2f Mon Sep 17 00:00:00 2001 From: matnad Date: Thu, 14 Feb 2019 17:48:24 +0100 Subject: [PATCH 1/5] issue #3 adding "commandline" feature --- cogs/help.py | 10 ++++- cogs/poll.py | 101 +++++++++++++++++++----------------------- cogs/poll_controls.py | 94 ++++++++++++++++++++++++++++++++++----- utils/misc.py | 29 ++++++++++++ 4 files changed, 166 insertions(+), 68 deletions(-) diff --git a/cogs/help.py b/cogs/help.py index bed434d..ffea820 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -49,7 +49,7 @@ class Help: if page == '🏠': ## POLL CREATION SHORT embed.add_field(name='🆕 Making New Polls', - value=f'`{pre}quick` | `{pre}new` | `{pre}prepare`', inline=False) + value=f'`{pre}quick` | `{pre}new` | `{pre}prepare` | `{pre}cmd `', 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: `` (optional)', inline=False) # embed.add_field(name='Examples', value=f'Examples: `{pre}new` | `{pre}quick What is the greenest color?`', @@ -108,6 +108,14 @@ class Help: 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 `', + 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`', + inline=False) + elif page == '🔍': embed.add_field(name='🔍 Show Polls', value='All users can display and list polls, with the exception of prepared polls. ' diff --git a/cogs/poll.py b/cogs/poll.py index 8ecf461..ec69370 100644 --- a/cogs/poll.py +++ b/cogs/poll.py @@ -165,7 +165,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() self.name = await get_valid(reply) await self.add_vaild(message, self.name) break @@ -203,7 +207,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() self.short = await get_valid(reply) await self.add_vaild(message, self.short) break @@ -263,7 +271,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() dt = await get_valid(reply) self.activation = dt if self.activation == 0: @@ -315,7 +327,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() self.anonymous = await get_valid(reply) await self.add_vaild(message, f'{"Yes" if self.anonymous else "No"}') break @@ -382,7 +398,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() self.multiple_choice = await get_valid(reply) await self.add_vaild(message, f'{"Yes" if self.multiple_choice else "No"}') break @@ -450,7 +470,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() options = await get_valid(reply) self.options_reaction_default = False if isinstance(options, int): @@ -527,7 +551,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() self.roles = await get_valid(reply, roles) await self.add_vaild(message, f'{", ".join(self.roles)}') break @@ -540,43 +568,6 @@ class Poll: except InvalidRoles as e: await self.add_error(message, f'**The following roles are invalid: {e.roles}**') - # async def set_options_traditional(self, force=None): - # '''Currently not used as everything is reaction based''' - # if force is not None: - # self.options_traditional = force - # return - # if self.stopped: return - # text = """**Next you chose from the possible set of options/answers for your poll.** - # Type the corresponding number or type your own options, separated by commas. - # - # **1** - yes, no - # **2** - in favour, against, abstain - # **3** - love it, like it, don't care, meh, hate it - # - # If you write your own options they will be listed and can be voted for. - # To use your custom options type them like this: - # **apple juice, banana ice cream, kiwi slices**""" - # message = await self.wizard_says(text) - # - # reply = '' - # while reply == '' or (reply.split(",").__len__() < 2 and reply not in ['1', '2', '3']) \ - # or (reply.split(",").__len__() > 99 and reply not in ['1', '2', '3']): - # if reply != '': - # await self.add_error(message, - # '**Invalid entry. Type `1` `2` or `3` or a comma separated list (max. 99 options).**') - # reply = await self.get_user_reply() - # if self.stopped: break - # - # if reply == '1': - # self.options_traditional = ['yes, no'] - # elif reply == '2': - # self.options_traditional = ['in favour', 'against', 'abstain'] - # elif reply == '3': - # self.options_traditional = ['love it', 'like it', 'don\'t care', 'meh', 'hate it'] - # else: - # self.options_traditional = [r.strip() for r in reply.split(",")] - # return self.options_traditional - async def set_weights(self, force=None): """Set role weights for the poll.""" async def get_valid(in_reply, server_roles): @@ -623,7 +614,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() w_n = await get_valid(reply, self.server.roles) self.weights_roles = w_n[0] self.weights_numbers = w_n[1] @@ -686,7 +681,11 @@ class Poll: while True: try: - reply = await self.get_user_reply() + if force: + reply = force + force = None + else: + reply = await self.get_user_reply() dt = await get_valid(reply) self.duration = dt if self.duration == 0: @@ -705,7 +704,6 @@ class Poll: def finalize(self): self.time_created = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) - async def to_dict(self): if self.channel is None: cid = 0 @@ -965,7 +963,7 @@ class Poll: # else: # embed = await self.add_field_custom(name='**Options**', value=', '.join(self.get_options()), embed=embed) - embed.set_footer(text='bot is in development') + # embed.set_footer(text='bot is in development') return embed @@ -994,12 +992,6 @@ class Poll: else: return msg - # def get_options(self): - # if self.reaction: - # return self.options_reaction - # else: - # return self.options_traditional - def get_duration_with_tz(self): if self.duration == 0: return 0 @@ -1077,7 +1069,6 @@ class Poll: else: return sum([1 for c in [u for u in self.votes] if option in self.votes[c]['choices']]) - async def vote(self, user, option, message): if not await self.is_open(): # refresh to show closed poll diff --git a/cogs/poll_controls.py b/cogs/poll_controls.py index 6cbee4a..da6f82a 100644 --- a/cogs/poll_controls.py +++ b/cogs/poll_controls.py @@ -1,9 +1,14 @@ +import argparse import copy import json import logging +import shlex + import discord from discord.ext import commands + +from utils.misc import CustomFormatter from .poll import Poll from utils.paginator import embed_list_paginated from essentials.multi_server import get_server_pre, ask_for_server, ask_for_channel @@ -34,14 +39,14 @@ class PollControls: async def say_error(self, ctx, error_text, footer_text=None): embed = discord.Embed(title='', description=error_text, colour=SETTINGS.color) - embed.set_author(name='Error', icon_url=SETTINGS.title_icon) + embed.set_author(name='Error', icon_url=SETTINGS.author_icon) if footer_text is not None: embed.set_footer(text=footer_text) await self.bot.say(embed=embed) async def say_embed(self, ctx, say_text='', title='Pollmaster', footer_text=None): embed = discord.Embed(title='', description=say_text, colour=SETTINGS.color) - embed.set_author(name=title, icon_url=SETTINGS.title_icon) + embed.set_author(name=title, icon_url=SETTINGS.author_icon) if footer_text is not None: embed.set_footer(text=footer_text) await self.bot.say(embed=embed) @@ -222,7 +227,7 @@ class PollControls: title = f' Listing {short} polls' embed = discord.Embed(title='', description='', colour=SETTINGS.color) - embed.set_author(name=title, icon_url=SETTINGS.title_icon) + embed.set_author(name=title, icon_url=SETTINGS.author_icon) # await self.bot.say(embed=await self.embed_list_paginated(polls, item_fct, embed)) # msg = await self.embed_list_paginated(ctx, polls, item_fct, embed, per_page=8) pre = await get_server_pre(self.bot, server) @@ -242,13 +247,76 @@ class PollControls: footer = f'Type {pre}show to display all polls' await self.say_error(ctx, error, footer) + @commands.command(pass_context=True) + async def cmd(self, ctx, *, cmd=None): + '''The old, command style way paired with the wizard.''' + server = await ask_for_server(self.bot, ctx.message) + if not server: + return + pre = await get_server_pre(self.bot, server) + + # generate the argparser and handle invalid stuff + descr = 'Accept poll settings via commandstring. \n\n' \ + '**Wrap all arguments in quotes like this:** \n' \ + f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \ + 'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \ + 'If an argument is invalid, the wizard will step in. ' \ + 'If the command string is invalid, you will get this error :)' + parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False) + parser.add_argument('-question', '-q') + parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id))) + parser.add_argument('-options', '-o') + parser.add_argument('-roles', '-r', default='all') + parser.add_argument('-weights', '-w', default='none') + parser.add_argument('-duration', '-d', default='0') + parser.add_argument('-anonymous', '-a', action="store_true") + parser.add_argument('-multiple_choice', '-mc', 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 + + try: + args = parser.parse_args(cmds) + except SystemExit: + await self.say_error(ctx, error_text=helpstring) + 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_multiple_choice(force=f'{"yes" if args.multiple_choice else "no"}') + await poll.set_options_reaction(force=args.options) + await poll.set_roles(force=args.roles) + await poll.set_weights(force=args.weights) + await poll.set_duration(force=args.duration) + + poll = await self.wizard(ctx, route, server) + if poll: + await poll.post_embed() + + @commands.command(pass_context=True) async def quick(self, ctx, *, cmd=None): '''Create a quick poll with just a question and some options. Parameters: (optional)''' + server = await ask_for_server(self.bot, ctx.message) + if not server: + return async def route(poll): await poll.set_name(force=cmd) - await poll.set_short(force=str(await generate_word(self.bot, ctx.message.server.id))) + await poll.set_short(force=str(await generate_word(self.bot, server.id))) await poll.set_anonymous(force='no') await poll.set_multiple_choice(force='no') await poll.set_options_reaction() @@ -256,13 +324,16 @@ class PollControls: await poll.set_weights(force='none') await poll.set_duration(force='0') - poll = await self.wizard(ctx, route) + poll = await self.wizard(ctx, route, server) if poll: await poll.post_embed() @commands.command(pass_context=True) async def prepare(self, ctx, *, cmd=None): '''Prepare a poll to use later. Parameters: (optional) ''' + server = await ask_for_server(self.bot, ctx.message) + if not server: + return async def route(poll): await poll.set_name(force=cmd) @@ -278,13 +349,16 @@ class PollControls: await poll.set_weights() await poll.set_duration() - poll = await self.wizard(ctx, route) + poll = await self.wizard(ctx, route, server) if poll: await poll.post_embed(destination=ctx.message.author) @commands.command(pass_context=True) async def new(self, ctx, *, cmd=None): '''Start the poll wizard to create a new poll step by step. Parameters: (optional) ''' + server = await ask_for_server(self.bot, ctx.message) + if not server: + return async def route(poll): await poll.set_name(force=cmd) @@ -299,16 +373,12 @@ class PollControls: await poll.set_weights() await poll.set_duration() - poll = await self.wizard(ctx, route) + poll = await self.wizard(ctx, route, server) if poll: await poll.post_embed() # The Wizard! - async def wizard(self, ctx, route): - server = await ask_for_server(self.bot, ctx.message) - if not server: - return - + async def wizard(self, ctx, route, server): channel = await ask_for_channel(self.bot, server, ctx.message) if not channel: return diff --git a/utils/misc.py b/utils/misc.py index a7ef100..26f9ba1 100644 --- a/utils/misc.py +++ b/utils/misc.py @@ -1,6 +1,35 @@ +import argparse + import pytz import datetime as dt + +class CustomFormatter(argparse.RawTextHelpFormatter): + def _format_action_invocation(self, action): + if not action.option_strings: + metavar, = self._metavar_formatter(action, action.dest)(1) + return metavar + else: + parts = [] + # if the Optional doesn't take a value, format is: + # -s, --long + if action.nargs == 0: + parts.extend(action.option_strings) + + # if the Optional takes a value, format is: + # -s ARGS, --long ARGS + # change to + # -s, --long ARGS + else: + default = action.dest.upper() + args_string = self._format_args(action, default) + for option_string in action.option_strings: + # parts.append('%s %s' % (option_string, args_string)) + parts.append('%s' % option_string) + parts[-1] += ' %s' % args_string + return ', '.join(parts) + + def possible_timezones(tz_offset, common_only=True): # pick one of the timezone collections timezones = pytz.common_timezones if common_only else pytz.all_timezones From a2be41dded907dcfbea26736b4d12f9f82fc1d4f Mon Sep 17 00:00:00 2001 From: matnad Date: Thu, 14 Feb 2019 19:05:38 +0100 Subject: [PATCH 2/5] issue #3 adding "commandline" feature --- cogs/poll.py | 132 +++++++++++++++++++++++++++-------------- cogs/poll_controls.py | 18 ++---- essentials/settings.py | 6 +- 3 files changed, 95 insertions(+), 61 deletions(-) diff --git a/cogs/poll.py b/cogs/poll.py index ec69370..b58f621 100644 --- a/cogs/poll.py +++ b/cogs/poll.py @@ -52,7 +52,7 @@ class Poll: self.short = str(uuid4())[0:23] self.anonymous = False self.reaction = True - self.multiple_choice = False + self.multiple_choice = 1 self.options_reaction = ['yes', 'no'] self.options_reaction_default = False # self.options_traditional = [] @@ -369,15 +369,15 @@ class Poll: 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 + elif not in_reply.isdigit(): + raise ExpectedInteger + elif int(in_reply) > self.options_reaction.__len__(): + raise OutOfRange + elif int(in_reply) <= self.options_reaction.__len__() >= 0: + return int(in_reply) else: raise InvalidInput @@ -387,13 +387,14 @@ class Poll: except InputError: pass - text = ("**Should users be able to vote for multiple options?**\n" + text = ("**How many options should the voters be able choose?**\n" "\n" - "`0 - No`\n" - "`1 - Yes`\n" + "`0 - No Limit: Multiple Choice`\n" + "`1 - Single Choice`\n" + "`2+ - Specify exactly how many Choices`\n" "\n" - "If you type `0` or `no`, a new vote will override the old vote. " - "Otherwise the users can vote for as many options as they like.") + "If the maximum choices are reached for a voter, they have to unvote an option before being able to " + "vote for a different one.") message = await self.wizard_says(text) while True: @@ -404,10 +405,14 @@ class Poll: else: reply = await self.get_user_reply() self.multiple_choice = await get_valid(reply) - await self.add_vaild(message, f'{"Yes" if self.multiple_choice else "No"}') + await self.add_vaild(message, f'{self.multiple_choice if self.multiple_choice > 0 else "No Limit"}') break except InvalidInput: - await self.add_error(message, '**You can only answer with `yes` | `1` or `no` | `0`!**') + await self.add_error(message, '**Invalid Input**') + except ExpectedInteger: + await self.add_error(message, '**Enter a positive number**') + except OutOfRange: + await self.add_error(message, '**You can\'t have more choices than options.**') async def set_options_reaction(self, force=None): @@ -842,7 +847,20 @@ class Poll: self.short = d['short'] self.anonymous = d['anonymous'] self.reaction = d['reaction'] - self.multiple_choice = d['multiple_choice'] + + # backwards compatibility for multiple choice + if isinstance(d['multiple_choice'], bool): + if d['multiple_choice']: + self.multiple_choice = 0 + else: + self.multiple_choice = 1 + else: + try: + self.multiple_choice = int(d['multiple_choice']) + except ValueError: + logger.exception('Multiple Choice not an int or bool.') + self.multiple_choice = 0 # default + self.options_reaction = d['options_reaction'] self.options_reaction_default = d['reaction_default'] # self.options_traditional = d['options_traditional'] @@ -936,8 +954,12 @@ class Poll: if self.options_reaction_default: if await self.is_open(): text = f'**Score** ' - text += '*(Multiple Choice)*' if self.multiple_choice \ - else '*(Single Choice)*' + if self.multiple_choice == 0: + text += f'(Multiple Choice)' + elif self.multiple_choice == 1: + text += f'(Single Choice)' + else: + text += f'({self.multiple_choice} Choices)' else: text = f'**Final Score**' @@ -949,10 +971,20 @@ class Poll: embed.add_field(name='\u200b', value='\u200b', inline=False) if await self.is_open(): text = f'*Vote by adding reactions to the poll*. ' - text += '*You can vote for multiple options.*' if self.multiple_choice \ - else '*You have 1 vote, but can change it.*' + if self.multiple_choice == 0: + text += '*You can vote for multiple options.*' + elif self.multiple_choice == 1: + text += '*You have 1 vote, but can change it.*' + else: + text += f'*You have {self.multiple_choice} choices and can change them.*' else: - text = f'*Final Results of the {"multiple choice" if self.multiple_choice else "single choice"} Poll.*' + text = f'*Final Results of the Poll *' + if self.multiple_choice == 0: + text += '*(Multiple Choice).*' + elif self.multiple_choice == 1: + text += '*(Single Choice).*' + else: + text += f'*(With up to {self.multiple_choice} choices).*' embed = await self.add_field_custom(name='**Options**', value=text, embed=embed) for i, r in enumerate(self.options_reaction): embed = await self.add_field_custom( @@ -1079,7 +1111,7 @@ class Poll: return choice = 'invalid' - already_voted = False + refresh_poll = True # get weight weight = 1 @@ -1103,18 +1135,27 @@ class Poll: choice = AZ_EMOJIS.index(option) if choice != 'invalid': - if self.multiple_choice: - if choice in self.votes[user.id]['choices'] and self.anonymous: - # anonymous multiple choice -> can't unreact so we toggle with react - await self.unvote(user, option, message) - return - self.votes[user.id]['choices'].append(choice) - # if len(self.votes[user.id]['choices']) > len(set(self.votes[user.id]['choices'])): - # already_voted = True - self.votes[user.id]['choices'] = list(set(self.votes[user.id]['choices'])) + if self.multiple_choice != 1: # more than 1 choice (0 = no limit) + if choice in self.votes[user.id]['choices']: + if self.anonymous: + # anonymous multiple choice -> can't unreact so we toggle with react + await self.unvote(user, option, message) + return + refresh_poll = False + else: + if self.multiple_choice > 0 and self.votes[user.id]['choices'].__len__() >= self.multiple_choice: + 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.' + embed = discord.Embed(title='', description=say_text, colour=SETTINGS.color) + embed.set_author(name='Pollmaster', icon_url=SETTINGS.author_icon) + await self.bot.send_message(user, embed=embed) + refresh_poll = False + else: + self.votes[user.id]['choices'].append(choice) + self.votes[user.id]['choices'] = list(set(self.votes[user.id]['choices'])) else: if [choice] == self.votes[user.id]['choices']: - already_voted = True + refresh_poll = False if self.anonymous: # undo anonymous vote await self.unvote(user, option, message) @@ -1128,7 +1169,7 @@ class Poll: await self.save_to_db() # refresh - if not already_voted: + if refresh_poll: # edit message if there is a real change await self.bot.edit_message(message, embed=await self.generate_embed()) @@ -1145,21 +1186,20 @@ class Poll: if str(user.id) not in self.votes: return choice = 'invalid' - if self.reaction: - if self.options_reaction_default: - if option in self.options_reaction: - choice = self.options_reaction.index(option) - else: - if option in AZ_EMOJIS: - choice = AZ_EMOJIS.index(option) + if self.options_reaction_default: + if option in self.options_reaction: + choice = self.options_reaction.index(option) + else: + if option in AZ_EMOJIS: + choice = AZ_EMOJIS.index(option) - if choice != 'invalid' and choice in self.votes[user.id]['choices']: - try: - self.votes[user.id]['choices'].remove(choice) - await self.save_to_db() - await self.bot.edit_message(message, embed=await self.generate_embed()) - except ValueError: - pass + if choice != 'invalid' and choice in self.votes[user.id]['choices']: + try: + self.votes[user.id]['choices'].remove(choice) + await self.save_to_db() + await self.bot.edit_message(message, embed=await self.generate_embed()) + except ValueError: + pass async def has_required_role(self, user): return not set([r.name for r in user.roles]).isdisjoint(self.roles) diff --git a/cogs/poll_controls.py b/cogs/poll_controls.py index da6f82a..f7b7a05 100644 --- a/cogs/poll_controls.py +++ b/cogs/poll_controls.py @@ -266,11 +266,11 @@ class PollControls: 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('-duration', '-d', default='0') parser.add_argument('-anonymous', '-a', action="store_true") - parser.add_argument('-multiple_choice', '-mc', action="store_true") helpstring = parser.format_help() helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ") @@ -296,8 +296,8 @@ class PollControls: 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_multiple_choice(force=f'{"yes" if args.multiple_choice 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.duration) @@ -318,8 +318,8 @@ class PollControls: await poll.set_name(force=cmd) await poll.set_short(force=str(await generate_word(self.bot, server.id))) await poll.set_anonymous(force='no') - await poll.set_multiple_choice(force='no') await poll.set_options_reaction() + await poll.set_multiple_choice(force='1') await poll.set_roles(force='all') await poll.set_weights(force='none') await poll.set_duration(force='0') @@ -340,11 +340,8 @@ class PollControls: await poll.set_short() await poll.set_preparation() await poll.set_anonymous() + await poll.set_options_reaction() await poll.set_multiple_choice() - if poll.reaction: - await poll.set_options_reaction() - else: - await poll.set_options_traditional() await poll.set_roles() await poll.set_weights() await poll.set_duration() @@ -364,11 +361,8 @@ class PollControls: await poll.set_name(force=cmd) await poll.set_short() await poll.set_anonymous() + await poll.set_options_reaction() await poll.set_multiple_choice() - if poll.reaction: - await poll.set_options_reaction() - else: - await poll.set_options_traditional() await poll.set_roles() await poll.set_weights() await poll.set_duration() @@ -547,7 +541,7 @@ class PollControls: if p.anonymous: # immediately remove reaction and to be safe, remove all reactions await self.bot.remove_reaction(message, emoji, user) - elif not p.multiple_choice: + elif p.multiple_choice == 1: # remove all other reactions for r in message.reactions: if r.emoji and r.emoji != emoji: diff --git a/essentials/settings.py b/essentials/settings.py index 912f46c..0bca921 100644 --- a/essentials/settings.py +++ b/essentials/settings.py @@ -6,9 +6,9 @@ from essentials.secrets import SECRETS class Settings: def __init__(self): self.color = discord.Colour(int('7289da', 16)) - self.title_icon = "http://mnadler.ch/img/tag.png" - self.author_icon = "http://mnadler.ch/img/tag.jpg" - self.report_icon = "http://mnadler.ch/img/report.png" + self.title_icon = "https://i.imgur.com/vtLsAl8.jpg" #PM + self.author_icon = "https://i.imgur.com/TYbBtwB.jpg" #tag + self.report_icon = "https://i.imgur.com/YksGRLN.png" #report self.owner_id = 117687652278468610 self.msg_errors = False self.log_errors = True From c733d1f4f2840d0f0a65a3bbec5cef7193c10ec2 Mon Sep 17 00:00:00 2001 From: matnad Date: Tue, 19 Feb 2019 17:01:24 +0100 Subject: [PATCH 3/5] issue #6 fix DM char limit for displaying channels --- cogs/db_api.py | 2 +- cogs/poll_controls.py | 2 +- essentials/multi_server.py | 21 +++++++++++++++++---- pollmaster.py | 7 +++++++ utils/paginator.py | 8 +++++--- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cogs/db_api.py b/cogs/db_api.py index 51fe1fc..cb9cb4b 100644 --- a/cogs/db_api.py +++ b/cogs/db_api.py @@ -20,7 +20,7 @@ class DiscordBotsOrgAPI: while True: logger.info('attempting to post server count') try: - await self.dblpy.post_server_count() + #await self.dblpy.post_server_count() logger.info('posted server count ({})'.format(len(self.bot.servers))) except Exception as e: logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e)) diff --git a/cogs/poll_controls.py b/cogs/poll_controls.py index f7b7a05..f61585b 100644 --- a/cogs/poll_controls.py +++ b/cogs/poll_controls.py @@ -222,7 +222,7 @@ class PollControls: else: return - def item_fct(item): + def item_fct(i,item): return f':black_small_square: **{item["short"]}**: {item["name"]}' title = f' Listing {short} polls' diff --git a/essentials/multi_server.py b/essentials/multi_server.py index c6cb007..8ff5a78 100644 --- a/essentials/multi_server.py +++ b/essentials/multi_server.py @@ -1,6 +1,10 @@ +import time + import discord from essentials.settings import SETTINGS +from utils.paginator import embed_list_paginated + async def get_pre(bot, message): '''Gets the prefix for a message.''' @@ -107,13 +111,22 @@ async def ask_for_channel(bot, server, message): return False # otherwise ask for a channel - text = 'Polls are bound to a specific channel on a server. Please select the channel for this poll by typing the corresponding number.\n' i = 1 + text = 'Polls are bound to a specific channel on a server. Please select the channel for this poll by typing the corresponding number.\n' for name in [c.name for c in channel_list]: - text += f'\n**{i}** - {name}' - i += 1 + to_add = f'\n**{i}** - {name}' + + # check if length doesn't exceed allowed maximum or split it into multiple messages + if text.__len__() + to_add.__len__() > 2048: + embed = discord.Embed(title="Select a channel", description=text, color=SETTINGS.color) + await bot.say(embed=embed) + text = 'Polls are bound to a specific channel on a server. Please select the channel for this poll by typing the corresponding number.\n' + else: + text += to_add + i += 1 + embed = discord.Embed(title="Select a channel", description=text, color=SETTINGS.color) - server_msg = await bot.say(embed=embed) + await bot.say(embed=embed) valid_reply = False nr = 1 diff --git a/pollmaster.py b/pollmaster.py index abc5613..9a445f3 100644 --- a/pollmaster.py +++ b/pollmaster.py @@ -55,6 +55,12 @@ async def on_ready(): print(bot.db) await bot.change_presence(game=discord.Game(name=f'V2 IS HERE >> pm!help')) + await bot.db.config.update_one( + {'_id': '261914618342014977'}, + {'$set': {'prefix': 'd!'}}, + upsert=True + ) + # check discord server configs try: db_server_ids = [entry['_id'] async for entry in bot.db.config.find({}, {})] @@ -97,6 +103,7 @@ async def on_command_error(e, ctx): # log error logger.error(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}') + raise e if SETTINGS.msg_errors: # send discord message for unexpected errors diff --git a/utils/paginator.py b/utils/paginator.py index c1d3385..771bc58 100644 --- a/utils/paginator.py +++ b/utils/paginator.py @@ -4,8 +4,9 @@ async def embed_list_paginated(bot, pre, items, item_fct, base_embed, footer_pre # generate list embed.title = f'{items.__len__()} entries' text = '\n' - for item in items[start:start+per_page]: - text += item_fct(item) + '\n' + for i,item in enumerate(items[start:start+per_page]): + j = i+start + text += item_fct(j,item) + '\n' embed.description = text # footer text @@ -21,7 +22,8 @@ async def embed_list_paginated(bot, pre, items, item_fct, base_embed, footer_pre # post / edit message if msg is not None: await bot.edit_message(msg, embed=embed) - await bot.clear_reactions(msg) + if str(msg.channel.type) != 'private': + await bot.clear_reactions(msg) else: msg = await bot.say(embed=embed) From f2fdecd1cda82e163bae0977edafb675c8f83806 Mon Sep 17 00:00:00 2001 From: matnad Date: Tue, 19 Feb 2019 17:02:34 +0100 Subject: [PATCH 4/5] issue #6 fix DM char limit for displaying channels --- cogs/db_api.py | 2 +- pollmaster.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/db_api.py b/cogs/db_api.py index cb9cb4b..51fe1fc 100644 --- a/cogs/db_api.py +++ b/cogs/db_api.py @@ -20,7 +20,7 @@ class DiscordBotsOrgAPI: while True: logger.info('attempting to post server count') try: - #await self.dblpy.post_server_count() + await self.dblpy.post_server_count() logger.info('posted server count ({})'.format(len(self.bot.servers))) except Exception as e: logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e)) diff --git a/pollmaster.py b/pollmaster.py index 9a445f3..4074a8a 100644 --- a/pollmaster.py +++ b/pollmaster.py @@ -103,7 +103,6 @@ async def on_command_error(e, ctx): # log error logger.error(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}') - raise e if SETTINGS.msg_errors: # send discord message for unexpected errors From 32a70bb01dee9374cdfbaa498c7ae7900af18765 Mon Sep 17 00:00:00 2001 From: matnad Date: Tue, 19 Feb 2019 18:30:03 +0100 Subject: [PATCH 5/5] added info feature --- changelog.md | 7 +++++ cogs/poll.py | 7 +++-- cogs/poll_controls.py | 67 +++++++++++++++++++++++++++++++++++++++++-- pollmaster.py | 8 +----- requirements.txt | 2 +- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index cd413cd..c6ad988 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +# Changelog for Version 2.1 + +## New features +- React to a poll with ❔ to get personalised info +- Added back the command line feature from version 1. Type *pm!cmd help* to get started +- Multiple choice is no longer a flag, but a number of how many options each voter can choose + # Changelog for Version 2.0 ## TL;DR diff --git a/cogs/poll.py b/cogs/poll.py index b58f621..461baed 100644 --- a/cogs/poll.py +++ b/cogs/poll.py @@ -467,7 +467,7 @@ class Poll: "**1** - :white_check_mark: :negative_squared_cross_mark:\n" "**2** - :thumbsup: :zipper_mouth: :thumbsdown:\n" "**3** - :heart_eyes: :thumbsup: :zipper_mouth: :thumbsdown: :nauseated_face:\n" - "**4** - in favour, against, abstain\n" + "**4** - in favour, against, abstaining\n" "\n" "Example for custom options:\n" "**apple juice, banana ice cream, kiwi slices** ") @@ -510,7 +510,7 @@ class Poll: if split.__len__() == 1 and split[0] in ['0', 'all', 'everyone']: return ['@everyone'] - if n_roles <= 20: + if n_roles <= 20 and force is None: if not all([r.isdigit() for r in split]): raise ExpectedInteger elif any([int(r) > n_roles for r in split]): @@ -1011,6 +1011,7 @@ class Poll: msg, r ) + await self.bot.add_reaction(msg, '❔') return msg else: for i, r in enumerate(self.options_reaction): @@ -1018,8 +1019,10 @@ class Poll: msg, AZ_EMOJIS[i] ) + await self.bot.add_reaction(msg, '❔') return msg elif not await self.is_open(): + await self.bot.add_reaction(msg, '❔') await self.bot.add_reaction(msg, '📎') else: return msg diff --git a/cogs/poll_controls.py b/cogs/poll_controls.py index f61585b..9e7a542 100644 --- a/cogs/poll_controls.py +++ b/cogs/poll_controls.py @@ -1,10 +1,12 @@ import argparse import copy +import datetime import json import logging import shlex import discord +import pytz from discord.ext import commands @@ -269,7 +271,7 @@ class PollControls: 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('-duration', '-d', default='0') + parser.add_argument('-deadline', '-d', default='0') parser.add_argument('-anonymous', '-a', action="store_true") helpstring = parser.format_help() @@ -300,7 +302,7 @@ class PollControls: 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.duration) + await poll.set_duration(force=args.deadline) poll = await self.wizard(ctx, route, server) if poll: @@ -524,8 +526,67 @@ class PollControls: ) return - # no rights, terminate function + # info member = server.get_member(user_id) + if emoji == '❔': + is_open = await p.is_open() + embed = discord.Embed(title=f"Info for the {'CLOSED ' if not is_open else ''}poll \"{p.name}\"", + description='', color=SETTINGS.color) + embed.set_author(name=f" >> {p.short}", icon_url=SETTINGS.author_icon) + + # 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?"}', + value=f'{"✅" if vote_rights else "❎"}', inline=False) + + # edit rights + edit_rights = False + if str(member.id) == str(p.author): + edit_rights = True + elif member.server_permissions.manage_server: + edit_rights = True + else: + result = await self.bot.db.config.find_one({'_id': str(server.id)}) + if result and result.get('admin_role') in [r.name for r in member.roles]: + edit_rights = True + embed.add_field(name='Can you manage the poll?', value=f'{"✅" if edit_rights else "❎"}', inline=False) + + # choices + choices = 'You have not voted yet.' if vote_rights else 'You can\'t vote in this poll.' + if user.id in p.votes: + if p.votes[user.id]['choices'].__len__() > 0: + choices = ', '.join([p.options_reaction[c] for c in p.votes[user.id]['choices']]) + embed.add_field(name=f'{"Your current votes (can be changed as long as the poll is open):" if is_open else "Your final votes:"}', + value=choices, inline=False) + + # weight + if vote_rights: + weight = 1 + if p.weights_roles.__len__() > 0: + valid_weights = [p.weights_numbers[p.weights_roles.index(r)] for r in + list(set([n.name for n in member.roles]).intersection(set(p.weights_roles)))] + if valid_weights.__len__() > 0: + weight = max(valid_weights) + else: + weight = 'You can\'t vote in this poll.' + embed.add_field(name='Weight of your votes:', value=weight, inline=False) + + # time left + deadline = p.get_duration_with_tz() + if not is_open: + time_left = 'This poll is closed.' + elif deadline == 0: + time_left = 'Until manually closed.' + else: + time_left = str(deadline-datetime.datetime.utcnow().replace(tzinfo=pytz.utc)).split('.', 2)[0] + + embed.add_field(name='Time left in the poll:', value=time_left, inline=False) + + await self.bot.send_message(user, embed=embed) + return + + # Assume: User wants to vote with reaction + # no rights, terminate function if not await p.has_required_role(member): await self.bot.remove_reaction(message, emoji, user) await self.bot.send_message(user, f'You are not allowed to vote in this poll. Only users with ' diff --git a/pollmaster.py b/pollmaster.py index 4074a8a..562ba6c 100644 --- a/pollmaster.py +++ b/pollmaster.py @@ -53,13 +53,7 @@ async def on_ready(): bot.db = mongo.pollmaster bot.session = aiohttp.ClientSession() print(bot.db) - await bot.change_presence(game=discord.Game(name=f'V2 IS HERE >> pm!help')) - - await bot.db.config.update_one( - {'_id': '261914618342014977'}, - {'$set': {'prefix': 'd!'}}, - upsert=True - ) + await bot.change_presence(game=discord.Game(name=f'pm!help - v2.1 is live!')) # check discord server configs try: diff --git a/requirements.txt b/requirements.txt index 90305e2..2227f7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.5.4 +aiohttp==1.0.5 async-timeout==3.0.1 attrs==18.2.0 beautifulsoup4==4.7.1