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/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..461baed 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 = [] @@ -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 @@ -353,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 @@ -371,23 +387,32 @@ 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: 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"}') + 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): @@ -442,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** ") @@ -450,7 +475,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): @@ -481,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]): @@ -527,7 +556,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 +573,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 +619,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 +686,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 +709,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 @@ -844,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'] @@ -938,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**' @@ -951,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( @@ -965,7 +995,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 @@ -981,6 +1011,7 @@ class Poll: msg, r ) + await self.bot.add_reaction(msg, '❔') return msg else: for i, r in enumerate(self.options_reaction): @@ -988,18 +1019,14 @@ 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 - # 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 +1104,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 @@ -1088,7 +1114,7 @@ class Poll: return choice = 'invalid' - already_voted = False + refresh_poll = True # get weight weight = 1 @@ -1112,18 +1138,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) @@ -1137,7 +1172,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()) @@ -1154,21 +1189,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 6cbee4a..9e7a542 100644 --- a/cogs/poll_controls.py +++ b/cogs/poll_controls.py @@ -1,9 +1,16 @@ +import argparse import copy +import datetime import json import logging +import shlex + import discord +import pytz 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 +41,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) @@ -217,12 +224,12 @@ 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' 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,73 +249,132 @@ 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('-multiple_choice', '-mc', default='1') + parser.add_argument('-roles', '-r', default='all') + parser.add_argument('-weights', '-w', default='none') + parser.add_argument('-deadline', '-d', default='0') + parser.add_argument('-anonymous', '-a', action="store_true") + + helpstring = parser.format_help() + helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ") + + if cmd and cmd == 'help': + await self.say_embed(ctx, say_text=helpstring) + return + + try: + cmds = shlex.split(cmd) + except ValueError: + await self.say_error(ctx, error_text=helpstring) + return + + 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_options_reaction(force=args.options) + await poll.set_multiple_choice(force=args.multiple_choice) + await poll.set_roles(force=args.roles) + await poll.set_weights(force=args.weights) + await poll.set_duration(force=args.deadline) + + poll = await self.wizard(ctx, route, server) + if poll: + await poll.post_embed() + + @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() + 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') - 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) 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() - 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) 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() - 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 @@ -460,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 ' @@ -477,7 +602,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/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/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 diff --git a/pollmaster.py b/pollmaster.py index abc5613..562ba6c 100644 --- a/pollmaster.py +++ b/pollmaster.py @@ -53,7 +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.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 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 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)