Initial commit
This commit is contained in:
commit
67ad573dd6
0
cogs/__init__.py
Normal file
0
cogs/__init__.py
Normal file
77
cogs/config.py
Normal file
77
cogs/config.py
Normal file
@ -0,0 +1,77 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.has_permissions(manage_server=True)
|
||||
async def prefix(self, ctx, *, pre):
|
||||
'''Set a custom prefix for the server.'''
|
||||
server = ctx.message.server
|
||||
# result = await self.bot.db.config.find_one({'_id': str(server_id)})
|
||||
# print(f'result: `{result}`')
|
||||
# if not result:
|
||||
# await self.bot.db.config.insert_one({'_id': str(server_id)}, {'$set': {'_id': str(server_id), 'prefix': str(pre)}})
|
||||
# self.bot.say(f'The server prefix has been set to `{pre}` Use `{pre}prefix <prefix>` to change it again.')
|
||||
# return
|
||||
#result['prefix'] = str(pre)
|
||||
|
||||
if pre.endswith('\w'):
|
||||
pre = pre[:-2]+' '
|
||||
msg = f'The server prefix has been set to `{pre}` Use `{pre}prefix <prefix>` to change it again.'
|
||||
else:
|
||||
msg = f'The server prefix has been set to `{pre}` Use `{pre}prefix <prefix>` to change it again. ' \
|
||||
f'If you would like to add a trailing whitespace to the prefix, use `{pre}prefix {pre}\w`.'
|
||||
|
||||
await self.bot.db.config.update_one({'_id': str(server.id)}, {'$set': {'prefix': str(pre)}}, upsert=True)
|
||||
await self.bot.say(msg)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.has_permissions(manage_server=True)
|
||||
async def adminrole(self, ctx, *, role=None):
|
||||
'''Set or show the Admin Role. Members with this role can create polls and manage ALL polls. Parameter: <role> (optional)'''
|
||||
server = ctx.message.server
|
||||
|
||||
if not role:
|
||||
result = await self.bot.db.config.find_one({'_id': str(server.id)})
|
||||
if result and result.get('admin_role'):
|
||||
await self.bot.say(f'The admin role restricts which users are able to create and manage ALL polls on this server. \n'
|
||||
f'The current admin role is `{result.get("admin_role")}`. '
|
||||
f'To change it type `{result.get("prefix")}adminrole <role name>`')
|
||||
else:
|
||||
await self.bot.say(f'The admin role restricts which users are able to create and manage ALL polls on this server. \n'
|
||||
f'No admin role set. '
|
||||
f'To set one type `{result["prefix"]}adminrole <role name>`')
|
||||
elif role in [r.name for r in server.roles]:
|
||||
await self.bot.db.config.update_one({'_id': str(server.id)}, {'$set': {'admin_role': str(role)}}, upsert=True)
|
||||
await self.bot.say(f'Server role `{role}` can now manage all polls.')
|
||||
else:
|
||||
await self.bot.say(f'Server role `{role}` not found.')
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
@commands.has_permissions(manage_server=True)
|
||||
async def userrole(self, ctx, *, role=None):
|
||||
'''Set or show the User Role. Members with this role can create polls and manage their own polls. Parameter: <role> (optional)'''
|
||||
server = ctx.message.server
|
||||
|
||||
if not role:
|
||||
result = await self.bot.db.config.find_one({'_id': str(server.id)})
|
||||
if result and result.get('user_role'):
|
||||
await self.bot.say(f'The user role restricts which users are able to create and manage their own polls. \n'
|
||||
f'The current user role is `{result.get("user_role")}`. '
|
||||
f'To change it type `{result.get("prefix")}userrole <role name>`')
|
||||
else:
|
||||
await self.bot.say(f'The user role restricts which users are able to create and manage their own polls. \n'
|
||||
f'No user role set. '
|
||||
f'To set one type `{result.get("prefix")}userrole <role name>`')
|
||||
elif role in [r.name for r in server.roles]:
|
||||
await self.bot.db.config.update_one({'_id': str(server.id)}, {'$set': {'user_role': str(role)}}, upsert=True)
|
||||
await self.bot.say(f'Server role `{role}` can now create and manage their own polls.')
|
||||
else:
|
||||
await self.bot.say(f'Server role `{role}` not found.')
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Config(bot))
|
||||
906
cogs/poll.py
Normal file
906
cogs/poll.py
Normal file
@ -0,0 +1,906 @@
|
||||
import codecs
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from math import ceil
|
||||
from uuid import uuid4
|
||||
from string import ascii_lowercase
|
||||
|
||||
from matplotlib import rcParams
|
||||
from matplotlib.afm import AFM
|
||||
from unidecode import unidecode
|
||||
|
||||
import discord
|
||||
from cogs.utils import get_pre
|
||||
from utils.poll_name_generator import generate_word
|
||||
|
||||
## Helvetica is the closest font to Whitney (discord uses Whitney) in afm
|
||||
## This is used to estimate text width and adjust the layout of the embeds'''
|
||||
afm_fname = os.path.join(rcParams['datapath'], 'fonts', 'afm', 'phvr8a.afm')
|
||||
with open(afm_fname, 'rb') as fh:
|
||||
afm = AFM(fh)
|
||||
|
||||
## A-Z Emojis for Discord
|
||||
AZ_EMOJIS = [(b'\\U0001f1a'.replace(b'a', bytes(hex(224 + (6 + i))[2:], "utf-8"))).decode("unicode-escape") for i in
|
||||
range(26)]
|
||||
|
||||
|
||||
class Poll:
|
||||
|
||||
def __init__(self, bot, ctx=None, server=None, channel=None, load=False):
|
||||
|
||||
self.bot = bot
|
||||
self.color = discord.Colour(int('7289da', 16))
|
||||
self.cursor_pos = 0
|
||||
|
||||
if not load and ctx:
|
||||
if server is None:
|
||||
server = ctx.message.server
|
||||
|
||||
if channel is None:
|
||||
channel = ctx.message.channel
|
||||
|
||||
self.author = ctx.message.author
|
||||
self.stopped = False
|
||||
|
||||
self.server = server
|
||||
self.channel = channel
|
||||
|
||||
self.name = "Quick Poll"
|
||||
self.short = str(uuid4())
|
||||
self.anonymous = False
|
||||
self.reaction = True
|
||||
self.multiple_choice = False
|
||||
self.options_reaction = ['yes', 'no']
|
||||
self.options_reaction_default = False
|
||||
self.options_traditional = []
|
||||
# self.options_traditional_default = False
|
||||
self.roles = ['@everyone']
|
||||
self.weights_roles = []
|
||||
self.weights_numbers = []
|
||||
self.duration = 0
|
||||
self.time_created = time.time()
|
||||
|
||||
self.open = True
|
||||
self.inactive = False
|
||||
self.votes = {}
|
||||
|
||||
def is_open(self):
|
||||
if open and self.duration > 0 and self.time_created + self.duration * 60 < time.time():
|
||||
self.open = False
|
||||
return self.open
|
||||
|
||||
async def wizard_says(self, text, footer=True):
|
||||
embed = discord.Embed(title="Poll creation Wizard", description=text,
|
||||
color=self.color)
|
||||
if footer: embed.set_footer(text="Type `stop` to cancel the wizard.")
|
||||
return await self.bot.say(embed=embed)
|
||||
|
||||
async def wizard_says_edit(self, message, text, add=False):
|
||||
if add and message.embeds.__len__() > 0:
|
||||
text = message.embeds[0]['description'] + text
|
||||
embed = discord.Embed(title="Poll creation Wizard", description=text,
|
||||
color=self.color)
|
||||
embed.set_footer(text="Type `stop` to cancel the wizard.")
|
||||
return await self.bot.edit_message(message, embed=embed)
|
||||
|
||||
async def add_error(self, message, error):
|
||||
text = ''
|
||||
if message.embeds.__len__() > 0:
|
||||
text = message.embeds[0]['description'] + '\n\n:exclamation: ' + error
|
||||
return await self.wizard_says_edit(message, text)
|
||||
|
||||
def check_reply(self, msg):
|
||||
if msg and msg.content:
|
||||
if msg.content.lower() == 'stop':
|
||||
self.stopped = True
|
||||
return msg
|
||||
elif msg.content.__len__() > 0:
|
||||
return msg
|
||||
else:
|
||||
return None
|
||||
|
||||
async def get_user_reply(self):
|
||||
reply = await self.bot.wait_for_message(author=self.author, check=self.check_reply)
|
||||
if self.stopped:
|
||||
await self.wizard_says('Poll Wizard stopped.', footer=False)
|
||||
if reply.content.startswith(await get_pre(self.bot, reply)):
|
||||
await self.wizard_says(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}`',
|
||||
footer=False)
|
||||
self.stopped = True
|
||||
return 'stop'
|
||||
return reply.content
|
||||
|
||||
async def set_name(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.name = force
|
||||
return
|
||||
text = """I will guide you step by step through the creation of your new poll.
|
||||
We can do this in a text channel or you can PM me to keep the server clean.
|
||||
**How would you like your poll to be called?**
|
||||
Try to be descriptive without writing more than one sentence."""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply.__len__() < 2 or reply.__len__() > 200:
|
||||
if reply != '':
|
||||
await self.add_error(message, '**Keep the name between 3 and 200 letters**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
self.name = reply
|
||||
return self.name
|
||||
|
||||
async def set_short(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.short = force
|
||||
return
|
||||
text = """Great. **Now type a unique one word identifier, a label, for your poll.**
|
||||
This label will be used to refer to the poll. Keep it short and significant."""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply.__len__() < 3 or reply.__len__() > 20 or reply.split(" ").__len__() != 1:
|
||||
if reply != '':
|
||||
await self.add_error(message, '**Only one word between 3 and 20 letters!**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if await self.bot.db.polls.find_one({'server_id': str(self.server.id), 'short': reply}) is not None:
|
||||
await self.add_error(message,
|
||||
f'**The label `{reply}` is not unique on this server. Choose a different one!**')
|
||||
reply = ''
|
||||
if reply in ['open', 'closed', 'prepared']:
|
||||
await self.add_error(message, '**Can\'t use reserved words (open, closed, prepared) as label!**')
|
||||
reply = ''
|
||||
|
||||
self.short = reply
|
||||
return self.short
|
||||
|
||||
async def set_anonymous(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.anonymous = force
|
||||
return
|
||||
text = """Next you need to decide: **Do you want your poll to be anonymous?**
|
||||
|
||||
`0 - No`
|
||||
`1 - Yes`
|
||||
|
||||
An anonymous poll has the following effects:
|
||||
:small_blue_diamond: You will never see who voted for which option
|
||||
:small_blue_diamond: Once the poll is closed, you will see who participated (but not their choice)"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply not in ['yes', 'no', '1', '0']:
|
||||
if reply != '':
|
||||
await self.add_error(message, '**You can only answer with `yes` | `1` or `no` | `0`!**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if isinstance(reply, str):
|
||||
reply = reply.lower()
|
||||
|
||||
self.anonymous = reply in ['yes', '1']
|
||||
return self.anonymous
|
||||
|
||||
async def set_reaction(self, force=None):
|
||||
''' Currently everything is reaction, this is not needed'''
|
||||
if force is not None:
|
||||
self.reaction = force
|
||||
return
|
||||
if self.stopped: return
|
||||
text = """**Do you want your users to vote by adding reactions to the poll? Type `yes` or `no`.**
|
||||
Reaction voting typically has the following properties:
|
||||
:small_blue_diamond: Voting is quick and painless (no typing required)
|
||||
:small_blue_diamond: Multiple votes are possible
|
||||
:small_blue_diamond: Not suited for a large number of options
|
||||
:small_blue_diamond: Not suited for long running polls"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply not in ['yes', 'no']:
|
||||
if reply != '':
|
||||
await self.add_error(message, '**You can only answer with `yes` or `no`!**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if isinstance(reply, str): reply = reply.lower()
|
||||
|
||||
self.reaction = reply == 'yes'
|
||||
return self.reaction
|
||||
|
||||
async def set_multiple_choice(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.multiple_choice = force
|
||||
return
|
||||
text = """**Should users be able to vote for multiple options?**
|
||||
|
||||
`0 - No`
|
||||
`1 - Yes`
|
||||
|
||||
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."""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply not in ['yes', 'no', '1', '0']:
|
||||
if reply != '':
|
||||
await self.add_error(message, '**You can only answer with `yes` | `1` or `no` | `0`!**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if isinstance(reply, str):
|
||||
reply = reply.lower()
|
||||
|
||||
self.multiple_choice = reply in ['yes', '1']
|
||||
return self.multiple_choice
|
||||
|
||||
async def set_options_reaction(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.options_reaction = force
|
||||
return
|
||||
text = """**Choose the options/answers for your poll.**
|
||||
Either type the corresponding number to a predefined option set,
|
||||
or type your own options, separated by commas.
|
||||
|
||||
**1** - :white_check_mark: :negative_squared_cross_mark:
|
||||
**2** - :thumbsup: :zipper_mouth: :thumbsdown:
|
||||
**3** - :heart_eyes: :thumbsup: :zipper_mouth: :thumbsdown: :nauseated_face:
|
||||
|
||||
Example for custom options:
|
||||
**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__() > 26 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. 26 options).**')
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
|
||||
if reply == '1':
|
||||
self.options_reaction = ['✅', '❎']
|
||||
self.options_reaction_default = True
|
||||
elif reply == '2':
|
||||
self.options_reaction = ['👍', '🤐', '👎']
|
||||
self.options_reaction_default = True
|
||||
elif reply == '3':
|
||||
self.options_reaction = ['😍', '👍', '🤐', '👎', '🤢']
|
||||
self.options_reaction_default = True
|
||||
else:
|
||||
self.options_reaction = [r.strip() for r in reply.split(",")]
|
||||
self.options_reaction_default = False
|
||||
return self.options_reaction
|
||||
|
||||
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_roles(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None:
|
||||
self.roles = force
|
||||
return
|
||||
n_roles = self.server.roles.__len__()
|
||||
if n_roles == 0:
|
||||
text = "No roles found on this server, skipping to the next step."
|
||||
self.roles = ['@everyone']
|
||||
elif n_roles <= 15:
|
||||
text = """**Choose which roles are allowed to vote.**
|
||||
Type `0`, `all` or `everyone` to have no restrictions.
|
||||
If you want multiple roles to be able to vote, separate the numbers with a comma.
|
||||
"""
|
||||
text += f'\n`{0} - no restrictions`'
|
||||
i = 1
|
||||
for role in [r.name for r in self.server.roles]:
|
||||
text += f'\n`{i} - {role}`'
|
||||
i += 1
|
||||
text += """
|
||||
|
||||
Example: `2, 3`
|
||||
"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
while reply == '' or reply == 'error' or reply == 'toolarge':
|
||||
if reply == 'error':
|
||||
await self.add_error(message, f'**Only positive numbers separated by commas are allowed!**')
|
||||
elif reply == 'toolarge':
|
||||
await self.add_error(message, f'**Only use valid numbers......**')
|
||||
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
|
||||
if reply in ['0', 'all', 'everyone']:
|
||||
break
|
||||
if not all([r.strip().isdigit() for r in reply.split(",")]):
|
||||
reply = 'error'
|
||||
continue
|
||||
elif any([int(r.strip()) > n_roles for r in reply.split(",")]):
|
||||
reply = 'toolarge'
|
||||
continue
|
||||
if reply in ['0', 'all', 'everyone', 'stop']:
|
||||
self.roles = ['@everyone']
|
||||
else:
|
||||
role_names = [r.name for r in self.server.roles]
|
||||
self.roles = [role_names[i - 1] for i in [int(r.strip()) for r in reply.split(",")]]
|
||||
|
||||
else:
|
||||
text = """**Choose which roles are allowed to vote.**
|
||||
Type `0`, `all` or `everyone` to have no restrictions.
|
||||
Type out the role names, separated by a comma, to restrict voting to specific roles:
|
||||
`moderators, Editors, vips` (hint: role names are case sensitive!)
|
||||
"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
reply = ''
|
||||
reply_roles = []
|
||||
while reply == '':
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if reply in ['0', 'all', 'everyone']:
|
||||
break
|
||||
reply_roles = [r.strip() for r in reply.split(",")]
|
||||
invalid_roles = []
|
||||
for r in reply_roles:
|
||||
if not any([r == sr for sr in [x.name for x in self.server.roles]]):
|
||||
invalid_roles.append(r)
|
||||
if invalid_roles.__len__() > 0:
|
||||
await self.add_error(message,
|
||||
f'**Invalid roles found: {", ".join(invalid_roles)}. Roles are case-sensitive!**')
|
||||
reply = ''
|
||||
continue
|
||||
|
||||
if reply in ['0', 'all', 'everyone']:
|
||||
self.roles = ['@everyone']
|
||||
else:
|
||||
self.roles = reply_roles
|
||||
return self.roles
|
||||
|
||||
async def set_weights(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None and len(force) == 2:
|
||||
self.weights_roles = force[0]
|
||||
self.weights_numbers = force[1]
|
||||
return
|
||||
text = """Almost done.
|
||||
**Weights allow you to give certain roles more or less effective votes.
|
||||
Type `0` or `none` if you don't need any weights.**
|
||||
A weight for the role `moderator` of `2` for example will automatically count the votes of all the moderators twice.
|
||||
To assign weights type the role, followed by a colon, followed by the weight like this:
|
||||
`moderator: 2, newbie: 0.5`"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
status = 'start'
|
||||
roles = []
|
||||
weights = []
|
||||
while status != 'ok':
|
||||
if status != 'start':
|
||||
await self.add_error(message, status)
|
||||
status = 'ok'
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
roles = []
|
||||
weights = []
|
||||
if reply.lower() in ['0', 'none']:
|
||||
break
|
||||
for e in [r.strip() for r in reply.split(",")]:
|
||||
if ':' in e:
|
||||
c = [x.strip() for x in e.rsplit(":", 1)]
|
||||
if not any([c[0] == sr for sr in [x.name for x in self.server.roles]]):
|
||||
status = f'**Role `{c[0]}` not found.**'
|
||||
break
|
||||
else:
|
||||
try:
|
||||
c[1] = float(c[1])
|
||||
except ValueError:
|
||||
status = f'**Weight `{c[1]}` for `{c[0]}` is not a number.**'
|
||||
break
|
||||
c[1] = int(c[1]) if c[1].is_integer() else c[1]
|
||||
roles.append(c[0])
|
||||
weights.append(c[1])
|
||||
if len(roles) > len(set(roles)):
|
||||
status = f'**Only assign a weight to a role once. `{c[0]}` is assigned twice.**'
|
||||
else:
|
||||
status = f'**No colon found in `{e}`. Use `:` to separate role and weight.**'
|
||||
break
|
||||
|
||||
self.weights_roles = roles
|
||||
self.weights_numbers = weights
|
||||
return [self.weights_roles, self.weights_numbers]
|
||||
|
||||
async def set_duration(self, force=None):
|
||||
if self.stopped: return
|
||||
if force is not None and isinstance(force, float):
|
||||
self.duration = force
|
||||
return
|
||||
text = """Last step.
|
||||
**How long should your poll be active?**
|
||||
Type a number followed by `m`inutes, `h`ours or `d`ays.
|
||||
If you want the poll to last indefinitely (until you close it), type `0`.
|
||||
You can also type a UTC closing date in this format: dd.mm.yyyy hh:mm
|
||||
|
||||
Examples: `5m` or `1 h` or `7d` or `15.8.2019 12:15`"""
|
||||
message = await self.wizard_says(text)
|
||||
|
||||
status = 'start'
|
||||
duration = 0.0
|
||||
while status != 'ok':
|
||||
if status != 'start':
|
||||
await self.add_error(message, status)
|
||||
status = 'ok'
|
||||
reply = await self.get_user_reply()
|
||||
if self.stopped: break
|
||||
if reply == '0': break
|
||||
|
||||
date = self.convert_user_date(reply)
|
||||
if isinstance(date, float):
|
||||
date = float(ceil(date))
|
||||
if date < 0:
|
||||
status = f'**The entered date is in the past. Use a date in the future.**'
|
||||
else:
|
||||
print(date)
|
||||
duration = date
|
||||
else:
|
||||
unit = reply[-1].lower()
|
||||
if unit not in ['m', 'h', 'd']:
|
||||
status = f'**{reply} is not a valid format.**'
|
||||
|
||||
duration = reply[:-1].strip()
|
||||
try:
|
||||
duration = float(duration)
|
||||
except ValueError:
|
||||
status = f'**{reply} is not a valid format.**'
|
||||
|
||||
if unit == 'h':
|
||||
duration *= 60
|
||||
elif unit == 'd':
|
||||
duration *= 60 * 24
|
||||
|
||||
self.duration = duration
|
||||
return self.duration
|
||||
|
||||
def convert_user_date(self, date):
|
||||
try:
|
||||
dt = datetime.datetime.strptime(date, '%d.%m.%Y %H:%M')
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
print(dt)
|
||||
print(datetime.datetime.utcnow())
|
||||
return float((dt - datetime.datetime.utcnow()).total_seconds() / 60.0)
|
||||
|
||||
def finalize(self):
|
||||
self.time_created = time.time()
|
||||
|
||||
# def __str__(self):
|
||||
# poll_string = f'Poll by user {self.author}\n'
|
||||
# poll_string += f'Name of the poll: {self.name}\n'
|
||||
# poll_string += f'Short of the poll: {self.short}\n'
|
||||
# poll_string += f'Short of the poll: {str(self.anonymous)}\n'
|
||||
# poll_string += f'Options reaction: {",".join(self.options_reaction)}\n'
|
||||
# poll_string += f'Options traditional: {",".join(self.options_traditional)}\n'
|
||||
# poll_string += f'Roles: {",".join(self.roles)}\n'
|
||||
# poll_string += f'WR: {",".join(self.weights_roles)}\n'
|
||||
# poll_string += f'WN: {",".join([str(x) for x in self.weights_numbers])}\n'
|
||||
# poll_string += f'duration: {self.duration} minutes\n'
|
||||
# return poll_string
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'server_id': str(self.server.id),
|
||||
'channel_id': str(self.channel.id),
|
||||
'author': str(self.author.id),
|
||||
'name': self.name,
|
||||
'short': self.short,
|
||||
'anonymous': self.anonymous,
|
||||
'reaction': self.reaction,
|
||||
'multiple_choice': self.multiple_choice,
|
||||
'options_reaction': self.options_reaction,
|
||||
'reaction_default': self.options_reaction_default,
|
||||
'options_traditional': self.options_traditional,
|
||||
'roles': self.roles,
|
||||
'weights_roles': self.weights_roles,
|
||||
'weights_numbers': self.weights_numbers,
|
||||
'duration': self.duration,
|
||||
'time_created': self.time_created,
|
||||
'open': self.open,
|
||||
'votes': self.votes
|
||||
}
|
||||
|
||||
def to_export(self):
|
||||
# export to txt file
|
||||
|
||||
# build string for weights
|
||||
weight_str = 'No weights'
|
||||
if self.weights_roles.__len__() > 0:
|
||||
weights = []
|
||||
for r, n in zip(self.weights_roles, self.weights_numbers):
|
||||
weights.append(f'{r}: {n}')
|
||||
weight_str = ', '.join(weights)
|
||||
|
||||
# Determine the poll winner
|
||||
winning_options = []
|
||||
winning_votes = 0
|
||||
for i, o in enumerate(self.options_reaction):
|
||||
votes = self.count_votes(i, weighted=True)
|
||||
if votes > winning_votes:
|
||||
winning_options = [o]
|
||||
winning_votes = votes
|
||||
elif votes == winning_votes:
|
||||
winning_options.append(o)
|
||||
|
||||
export = (f'--------------------------------------------\n'
|
||||
f'POLLMASTER DISCORD EXPORT\n'
|
||||
f'--------------------------------------------\n'
|
||||
f'Server name (ID): {self.server.name} ({self.server.id})\n'
|
||||
f'Owner of the poll: {self.author.name}\n'
|
||||
f'Time of creation: {datetime.datetime.utcfromtimestamp(self.time_created).strftime("%d-%b-%Y %H:%M") + " UTC"}\n'
|
||||
f'--------------------------------------------\n'
|
||||
f'POLL SETTINGS\n'
|
||||
f'--------------------------------------------\n'
|
||||
f'Question / Name: {self.name}\n'
|
||||
f'Label: {self.short}\n'
|
||||
f'Anonymous: {"Yes" if self.anonymous else "No"}\n'
|
||||
f'Multiple choice: {"Yes" if self.multiple_choice else "No"}\n'
|
||||
f'Answer options: {", ".join(self.options_reaction)}\n'
|
||||
f'Allowed roles: {", ".join(self.roles) if self.roles.__len__() > 0 else "@everyone"}\n'
|
||||
f'Weights for roles: {weight_str}\n'
|
||||
f'Deadline: {self.get_deadline(string=True)}\n'
|
||||
f'--------------------------------------------\n'
|
||||
f'POLL RESULTS\n'
|
||||
f'--------------------------------------------\n'
|
||||
f'Number of participants: {self.votes.__len__()}\n'
|
||||
f'Raw results: {", ".join([str(o)+": "+str(self.count_votes(i, weighted=False)) for i,o in enumerate(self.options_reaction)])}\n'
|
||||
f'Weighted results: {", ".join([str(o)+": "+str(self.count_votes(i, weighted=True)) for i,o in enumerate(self.options_reaction)])}\n'
|
||||
f'Winning option{"s" if winning_options.__len__() > 1 else ""}: {", ".join(winning_options)} with {winning_votes} votes\n')
|
||||
|
||||
if not self.anonymous:
|
||||
export += '--------------------------------------------\n' \
|
||||
'DETAILED POLL RESULTS\n' \
|
||||
'--------------------------------------------'
|
||||
|
||||
for user_id in self.votes:
|
||||
member = self.server.get_member(user_id)
|
||||
if self.votes[user_id]['choices'].__len__() == 0:
|
||||
continue
|
||||
export += f'\n{member.name}'
|
||||
if self.votes[user_id]['weight'] != 1:
|
||||
export += f' (weight: {self.votes[user_id]["weight"]})'
|
||||
export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']])
|
||||
export += '\n'
|
||||
else:
|
||||
export += '--------------------------------------------\n' \
|
||||
'LIST OF PARTICIPANTS\n' \
|
||||
'--------------------------------------------'
|
||||
|
||||
for user_id in self.votes:
|
||||
member = self.server.get_member(user_id)
|
||||
if self.votes[user_id]['choices'].__len__() == 0:
|
||||
continue
|
||||
export += f'\n{member.name}'
|
||||
if self.votes[user_id]['weight'] != 1:
|
||||
export += f' (weight: {self.votes[user_id]["weight"]})'
|
||||
# export += ': ' + ', '.join([self.options_reaction[c] for c in self.votes[user_id]['choices']])
|
||||
export += '\n'
|
||||
|
||||
export += ('--------------------------------------------\n'
|
||||
'BOT DETAILS\n'
|
||||
'--------------------------------------------\n'
|
||||
'Creator: Newti#0654\n'
|
||||
'Link to invite, vote for or support Pollmaster:\n'
|
||||
'https://discordbots.org/bot/444514223075360800\n'
|
||||
'--------------------------------------------\n'
|
||||
'END OF FILE\n'
|
||||
'--------------------------------------------\n')
|
||||
return export
|
||||
|
||||
def export(self):
|
||||
if not self.open:
|
||||
fn = 'export/' + str(self.server.id) + '_' + str(self.short) + '.txt'
|
||||
with codecs.open(fn, 'w', 'utf-8') as outfile:
|
||||
outfile.write(self.to_export())
|
||||
return fn
|
||||
else:
|
||||
return None
|
||||
|
||||
async def from_dict(self, d):
|
||||
self.server = self.bot.get_server(d['server_id'])
|
||||
self.channel = self.bot.get_channel(d['channel_id'])
|
||||
self.author = await self.bot.get_user_info(d['author'])
|
||||
self.name = d['name']
|
||||
self.short = d['short']
|
||||
self.anonymous = d['anonymous']
|
||||
self.reaction = d['reaction']
|
||||
self.multiple_choice = d['multiple_choice']
|
||||
self.options_reaction = d['options_reaction']
|
||||
self.options_reaction_default = d['reaction_default']
|
||||
self.options_traditional = d['options_traditional']
|
||||
self.roles = d['roles']
|
||||
self.weights_roles = d['weights_roles']
|
||||
self.weights_numbers = d['weights_numbers']
|
||||
self.duration = d['duration']
|
||||
self.time_created = d['time_created']
|
||||
self.open = d['open']
|
||||
self.open = self.is_open() # ckeck for deadline since last call
|
||||
self.cursor_pos = 0
|
||||
self.votes = d['votes']
|
||||
|
||||
async def save_to_db(self):
|
||||
await self.bot.db.polls.update_one({'server_id': str(self.server.id), 'short': str(self.short)},
|
||||
{'$set': self.to_dict()}, upsert=True)
|
||||
|
||||
@staticmethod
|
||||
async def load_from_db(bot, server_id, short, ctx=None, ):
|
||||
# query = await bot.db.polls.find_one({'server_id': str(server_id), 'short': str(short)})
|
||||
query = await bot.db.polls.find_one({'server_id': str(server_id), 'short': str(short)})
|
||||
if query is not None:
|
||||
p = Poll(bot, ctx, load=True)
|
||||
await p.from_dict(query)
|
||||
return p
|
||||
else:
|
||||
return None
|
||||
|
||||
async def add_field_custom(self, name, value, embed):
|
||||
## this is used to estimate the width of text and add empty embed fields for a cleaner report
|
||||
## cursor_pos is used to track if we are at the start of a new line in the report. Each line has max 2 slots for info.
|
||||
## If the line is short, we can fit a second field, if it is too long, we get an automatic linebreak.
|
||||
## If it is in between, we create an empty field to prevent the inline from looking ugly
|
||||
|
||||
name = str(name)
|
||||
value = str(value)
|
||||
|
||||
nwidth = afm.string_width_height(unidecode(name))
|
||||
vwidth = afm.string_width_height(unidecode(value))
|
||||
w = max(nwidth[0], vwidth[0])
|
||||
|
||||
embed.add_field(name=name, value=value, inline=False if w > 12500 and self.cursor_pos % 2 == 1 else True)
|
||||
self.cursor_pos += 1
|
||||
|
||||
## create an empty field if we are at the second slot and the width of the first slot is between the critical values
|
||||
if self.cursor_pos % 2 == 1 and w > 11600 and w < 20000:
|
||||
embed.add_field(name='\u200b', value='\u200b', inline=True)
|
||||
self.cursor_pos += 1
|
||||
|
||||
return embed
|
||||
|
||||
async def generate_embed(self):
|
||||
self.cursor_pos = 0
|
||||
embed = discord.Embed(title='', colour=self.color) # f'Status: {"Open" if self.is_open() else "Closed"}'
|
||||
embed.set_author(name=f' >> {self.short} ',
|
||||
icon_url="http://mnadler.ch/img/donat-chart-32.png")
|
||||
embed.set_thumbnail(url="http://mnadler.ch/img/poll-topic-64.png")
|
||||
|
||||
# ## adding fields with custom, length sensitive function
|
||||
embed = await self.add_field_custom(name='**Poll Question**', value=self.name, embed=embed)
|
||||
|
||||
embed = await self.add_field_custom(name='**Roles**', value=', '.join(self.roles), embed=embed)
|
||||
if len(self.weights_roles) > 0:
|
||||
weights = []
|
||||
for r, n in zip(self.weights_roles, self.weights_numbers):
|
||||
weights.append(f'{r}: {n}')
|
||||
embed = await self.add_field_custom(name='**Weights**', value=', '.join(weights), embed=embed)
|
||||
|
||||
embed = await self.add_field_custom(name='**Anonymous**', value=self.anonymous, embed=embed)
|
||||
|
||||
# embed = await self.add_field_custom(name='**Multiple Choice**', value=self.multiple_choice, embed=embed)
|
||||
embed = await self.add_field_custom(name='**Deadline**', value=self.get_poll_status(), embed=embed)
|
||||
embed = await self.add_field_custom(name='**Author**', value=self.author.name, embed=embed)
|
||||
|
||||
if self.reaction:
|
||||
if self.options_reaction_default:
|
||||
if 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.*'
|
||||
else:
|
||||
text = f'*Final Results of the {"multiple choice" if self.multiple_choice else "single choice"} Poll.*'
|
||||
|
||||
vote_display = []
|
||||
for i, r in enumerate(self.options_reaction):
|
||||
vote_display.append(f'{r} {self.count_votes(i)}')
|
||||
embed = await self.add_field_custom(name='**Score**', value=' '.join(vote_display), embed=embed)
|
||||
else:
|
||||
embed.add_field(name='\u200b', value='\u200b', inline=False)
|
||||
if 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.*'
|
||||
else:
|
||||
text = f'*Final Results of the {"multiple choice" if self.multiple_choice else "single choice"} Poll.*'
|
||||
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(
|
||||
name=f':regional_indicator_{ascii_lowercase[i]}: {self.count_votes(i)}',
|
||||
value=r,
|
||||
embed=embed
|
||||
)
|
||||
else:
|
||||
embed = await self.add_field_custom(name='**Options**', value=', '.join(self.get_options()), embed=embed)
|
||||
|
||||
embed.set_footer(text='bot is in development')
|
||||
|
||||
return embed
|
||||
|
||||
async def post_embed(self, ctx):
|
||||
msg = await self.bot.say(embed=await self.generate_embed())
|
||||
if self.reaction and self.is_open():
|
||||
if self.options_reaction_default:
|
||||
for r in self.options_reaction:
|
||||
await self.bot.add_reaction(
|
||||
msg,
|
||||
r
|
||||
)
|
||||
return msg
|
||||
else:
|
||||
for i, r in enumerate(self.options_reaction):
|
||||
await self.bot.add_reaction(
|
||||
msg,
|
||||
AZ_EMOJIS[i]
|
||||
)
|
||||
return msg
|
||||
else:
|
||||
return msg
|
||||
|
||||
def get_options(self):
|
||||
if self.reaction:
|
||||
return self.options_reaction
|
||||
else:
|
||||
return self.options_traditional
|
||||
|
||||
def get_deadline(self, string=False):
|
||||
deadline = float(self.time_created) + float(self.duration) * 60
|
||||
if string:
|
||||
if self.duration == 0:
|
||||
return 'No deadline'
|
||||
else:
|
||||
return datetime.datetime.utcfromtimestamp(deadline).strftime('%d-%b-%Y %H:%M') + ' UTC'
|
||||
else:
|
||||
return deadline
|
||||
|
||||
def get_poll_status(self):
|
||||
if self.is_open():
|
||||
return self.get_deadline(string=True)
|
||||
else:
|
||||
return 'Poll is closed.'
|
||||
|
||||
def count_votes(self, option, weighted=True):
|
||||
'''option: number from 0 to n'''
|
||||
if weighted:
|
||||
return sum([self.votes[c]['weight'] for c in [u for u in self.votes] if option in self.votes[c]['choices']])
|
||||
else:
|
||||
return sum([1 for c in [u for u in self.votes] if option in self.votes[c]['choices']])
|
||||
|
||||
# async def has_voted(self, user_id):
|
||||
# query = await self.bot.db.polls.find_one({'server_id': str(self.server.id), 'short': self.short})
|
||||
# if query is None:
|
||||
# return False
|
||||
# else:
|
||||
# votes = query['votes']
|
||||
# if user_id in votes:
|
||||
# return votes[user_id]
|
||||
# else:
|
||||
# return False
|
||||
|
||||
async def vote(self, user, option, message):
|
||||
if not self.is_open():
|
||||
# refresh to show closed poll
|
||||
await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
await self.bot.clear_reactions(message)
|
||||
return
|
||||
|
||||
choice = 'invalid'
|
||||
already_voted = False
|
||||
|
||||
# get weight
|
||||
weight = 1
|
||||
if self.weights_roles.__len__() > 0:
|
||||
valid_weights = [self.weights_numbers[self.weights_roles.index(r)] for r in
|
||||
list(set([n.name for n in user.roles]).intersection(set(self.weights_roles)))]
|
||||
if valid_weights.__len__() > 0:
|
||||
weight = max(valid_weights)
|
||||
|
||||
if str(user.id) not in self.votes:
|
||||
self.votes[user.id] = {'weight': weight, 'choices': []}
|
||||
else:
|
||||
self.votes[user.id]['weight'] = weight
|
||||
|
||||
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 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']))
|
||||
else:
|
||||
if [choice] == self.votes[user.id]['choices']:
|
||||
already_voted = True
|
||||
else:
|
||||
self.votes[user.id]['choices'] = [choice]
|
||||
else:
|
||||
pass
|
||||
|
||||
# commit
|
||||
await self.save_to_db()
|
||||
|
||||
# refresh
|
||||
if not already_voted:
|
||||
# edit message if there is a real change
|
||||
await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
pass
|
||||
|
||||
async def unvote(self, user, option, message):
|
||||
if not self.is_open():
|
||||
# refresh to show closed poll
|
||||
await self.bot.edit_message(message, embed=await self.generate_embed())
|
||||
await self.bot.clear_reactions(message)
|
||||
return
|
||||
|
||||
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 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)
|
||||
397
cogs/poll_controls.py
Normal file
397
cogs/poll_controls.py
Normal file
@ -0,0 +1,397 @@
|
||||
import copy
|
||||
import pprint
|
||||
import time
|
||||
|
||||
import discord
|
||||
from settings import *
|
||||
from discord.ext import commands
|
||||
from .poll import Poll
|
||||
from .utils import ask_for_server, ask_for_channel, get_server_pre
|
||||
from .utils import SETTINGS
|
||||
from utils.poll_name_generator import generate_word
|
||||
|
||||
|
||||
|
||||
class PollControls:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
## General Methods
|
||||
async def is_admin_or_creator(self, ctx, server, owner_id, error_msg=None):
|
||||
member = server.get_member(ctx.message.author.id)
|
||||
if member.id == owner_id:
|
||||
return 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]:
|
||||
return True
|
||||
else:
|
||||
if error_msg is not None:
|
||||
await self.bot.send_message(ctx.message.author, error_msg)
|
||||
return False
|
||||
|
||||
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)
|
||||
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)
|
||||
if footer_text is not None:
|
||||
embed.set_footer(text=footer_text)
|
||||
await self.bot.say(embed=embed)
|
||||
|
||||
## Commands
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
async def delete(self, ctx, *, short=None):
|
||||
'''Delete 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.server)
|
||||
error = f'Please specify the label of a poll after the close command. \n' \
|
||||
f'`{pre}close <poll_label>`'
|
||||
await self.say_error(ctx, error)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
if p is not None:
|
||||
# Permission Check: Admin or Creator
|
||||
if not await self.is_admin_or_creator(
|
||||
ctx, server,
|
||||
p.author.id,
|
||||
'You don\'t have sufficient rights to delete this poll. Please talk to the server admin.'
|
||||
):
|
||||
return False
|
||||
|
||||
# Delete Poll
|
||||
result = await self.bot.db.polls.delete_one({'server_id': server.id, 'short': short})
|
||||
if result.deleted_count == 1:
|
||||
say = f'Poll with label "{short}" was successfully deleted. This action can\'t be undone!'
|
||||
title = 'Poll deleted'
|
||||
await self.say_embed(ctx, say, title)
|
||||
else:
|
||||
error = f'Action failed. Poll could not be deleted. You should probably report his error to the dev, thanks!`'
|
||||
await self.say_error(ctx, error)
|
||||
|
||||
else:
|
||||
error = f'Poll with label "{short}" was not found.'
|
||||
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, footer)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
async def close(self, ctx, *, short=None):
|
||||
'''Close 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.server)
|
||||
error = f'Please specify the label of a poll after the close command. \n' \
|
||||
f'`{pre}close <poll_label>`'
|
||||
await self.say_error(ctx, error)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
if p is not None:
|
||||
# Permission Check: Admin or Creator
|
||||
if not await self.is_admin_or_creator(
|
||||
ctx, server,
|
||||
p.author.id,
|
||||
'You don\'t have sufficient rights to close this poll. Please talk to the server admin.'
|
||||
):
|
||||
return False
|
||||
|
||||
# Close Poll
|
||||
p.open = False
|
||||
await p.save_to_db()
|
||||
await ctx.invoke(self.show, short)
|
||||
else:
|
||||
error = f'Poll with label "{short}" was not found.'
|
||||
# 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(pass_context=True)
|
||||
async def export(self, ctx, *, short=None):
|
||||
'''Export 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.server)
|
||||
error = f'Please specify the label of a poll after the close command. \n' \
|
||||
f'`{pre}close <poll_label>`'
|
||||
await self.say_error(ctx, error)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
if p is not None:
|
||||
if p.open:
|
||||
pre = await get_server_pre(self.bot, ctx.message.server)
|
||||
error_text = f'You can only export closed polls. \nPlease `{pre}close {short}` the poll first or wait for the deadline.'
|
||||
await self.say_error(ctx, error_text)
|
||||
else:
|
||||
# sending file
|
||||
file = p.export()
|
||||
if file is not None:
|
||||
await self.bot.send_file(
|
||||
ctx.message.author,
|
||||
file,
|
||||
content='Sending you the requested export of "{}".'.format(p.short)
|
||||
)
|
||||
else:
|
||||
error_text = 'Could not export the requested poll. \nPlease report this to the developer.'
|
||||
await self.say_error(ctx, error_text)
|
||||
else:
|
||||
error = f'Poll with label "{short}" was not found.'
|
||||
# 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(pass_context=True)
|
||||
async def show(self, ctx, short='open', start=0):
|
||||
'''Show a list of open polls or show a specific poll. Parameters: "open" (default), "closed", "prepared" or <label>'''
|
||||
|
||||
server = await ask_for_server(self.bot, ctx.message, short)
|
||||
if not server:
|
||||
return
|
||||
|
||||
if short in ['open', 'closed', 'prepared']:
|
||||
query = None
|
||||
if short == 'open':
|
||||
query = self.bot.db.polls.find({'server_id': str(server.id), 'open': True})
|
||||
elif short == 'closed':
|
||||
query = self.bot.db.polls.find({'server_id': str(server.id), 'open': False})
|
||||
elif short == 'prepared':
|
||||
pass #TODO: prepared showw
|
||||
|
||||
if query is not None:
|
||||
polls = [poll async for poll in query]
|
||||
else:
|
||||
return
|
||||
|
||||
def item_fct(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)
|
||||
# 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)
|
||||
else:
|
||||
p = await Poll.load_from_db(self.bot, str(server.id), short)
|
||||
if p is not None:
|
||||
msg = await p.post_embed(ctx)
|
||||
else:
|
||||
error = f'Poll with label {short} was not found.'
|
||||
pre = await get_server_pre(self.bot, server)
|
||||
footer = f'Type {pre}show to display all polls'
|
||||
await self.say_error(ctx, error, footer)
|
||||
|
||||
@commands.command(pass_context=True)
|
||||
async def quick(self, ctx, *, cmd=None):
|
||||
'''Create a quick poll with just a question and some options. Parameters: <Question> (optional)'''
|
||||
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_anonymous(force=False)
|
||||
await poll.set_reaction(force=True)
|
||||
await poll.set_multiple_choice(force=False)
|
||||
await poll.set_options_reaction()
|
||||
await poll.set_roles(force=['@everyone'])
|
||||
await poll.set_weights(force=[[], []])
|
||||
await poll.set_duration(force=0.0)
|
||||
|
||||
await self.wizard(ctx, route)
|
||||
|
||||
@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: >Question> (optional) '''
|
||||
async def route(poll):
|
||||
await poll.set_name(force=cmd)
|
||||
await poll.set_short()
|
||||
await poll.set_anonymous()
|
||||
# await poll.set_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()
|
||||
|
||||
await self.wizard(ctx, route)
|
||||
|
||||
## Other methods
|
||||
async def embed_list_paginated(self, ctx, items, item_fct, base_embed, msg=None, start=0, per_page=10):
|
||||
embed = base_embed
|
||||
|
||||
# generate list
|
||||
embed.title = f'{items.__len__()} entries'
|
||||
text = '\n'
|
||||
for item in items[start:start+per_page]:
|
||||
text += item_fct(item) + '\n'
|
||||
embed.description = text
|
||||
|
||||
# footer text
|
||||
pre = await get_server_pre(self.bot, ctx.message.server)
|
||||
footer_text = f'Type {pre}show <label> to show a poll. '
|
||||
if start > 0:
|
||||
footer_text += f'React with ⏪ to show the last {per_page} entries. '
|
||||
if items.__len__() > start+per_page:
|
||||
footer_text += f'React with ⏩ to show the next {per_page} entries. '
|
||||
if footer_text.__len__() > 0:
|
||||
embed.set_footer(text=footer_text)
|
||||
|
||||
# post / edit message
|
||||
if msg is not None:
|
||||
await self.bot.edit_message(msg, embed=embed)
|
||||
await self.bot.clear_reactions(msg)
|
||||
else:
|
||||
msg = await self.bot.say(embed=embed)
|
||||
|
||||
# add reactions
|
||||
if start > 0:
|
||||
await self.bot.add_reaction(msg, '⏪')
|
||||
if items.__len__() > start+per_page:
|
||||
await self.bot.add_reaction(msg, '⏩')
|
||||
|
||||
# wait for reactions (2 minutes)
|
||||
def check(reaction, user):
|
||||
return reaction.emoji if user != self.bot.user else False
|
||||
res = await self.bot.wait_for_reaction(emoji=['⏪', '⏩'], message=msg, timeout=120, check=check)
|
||||
|
||||
# redirect on reaction
|
||||
if res is None:
|
||||
return
|
||||
elif res.reaction.emoji == '⏪' and start > 0:
|
||||
await self.embed_list_paginated(ctx, items, item_fct, base_embed, msg=msg, start=start-per_page, per_page=per_page)
|
||||
elif res.reaction.emoji == '⏩' and items.__len__() > start+per_page:
|
||||
await self.embed_list_paginated(ctx, items, item_fct, base_embed, msg=msg, start=start+per_page, per_page=per_page)
|
||||
|
||||
|
||||
|
||||
async def wizard(self, ctx, route):
|
||||
server = await ask_for_server(self.bot, ctx.message)
|
||||
if not server:
|
||||
return
|
||||
|
||||
channel = await ask_for_channel(self.bot, server, ctx.message)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
# Permission Check
|
||||
member = server.get_member(ctx.message.author.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('user_role') not in [r.name for r in member.roles]:
|
||||
await self.bot.send_message(ctx.message.author,
|
||||
'You don\'t have sufficient rights to start new polls on this server. Please talk to the server admin.')
|
||||
return
|
||||
|
||||
## Create object
|
||||
poll = Poll(self.bot, ctx, server, channel)
|
||||
|
||||
## Route to define object, passed as argument for different constructors
|
||||
await route(poll)
|
||||
|
||||
## Finalize
|
||||
if poll.stopped:
|
||||
print("Poll Wizard Stopped.")
|
||||
else:
|
||||
msg = await poll.post_embed(ctx)
|
||||
await poll.save_to_db()
|
||||
|
||||
|
||||
## BOT EVENTS (@bot.event)
|
||||
async def on_reaction_add(self, reaction, user):
|
||||
if user != self.bot.user:
|
||||
|
||||
if reaction.emoji.startswith(('⏪', '⏩')):
|
||||
return
|
||||
|
||||
# only look at our polls
|
||||
try:
|
||||
short = reaction.message.embeds[0]['author']['name'][3:]
|
||||
if not reaction.message.embeds[0]['author']['name'].startswith('>> ') or not short:
|
||||
return
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
# create message object for the reaction
|
||||
user_msg = copy.deepcopy(reaction.message)
|
||||
user_msg.author = user
|
||||
|
||||
server = await ask_for_server(self.bot, user_msg, short)
|
||||
if str(user_msg.channel.type) == 'private':
|
||||
user = server.get_member(user.id)
|
||||
user_msg.author = user
|
||||
|
||||
# fetch poll
|
||||
p = await Poll.load_from_db(self.bot, server.id, short)
|
||||
if p is None:
|
||||
return
|
||||
# no rights, terminate function
|
||||
if not await p.has_required_role(user):
|
||||
await self.bot.remove_reaction(reaction.message, reaction.emoji, user)
|
||||
await self.bot.send_message(user, f'You are not allowed to vote in this poll. Only users with '
|
||||
f'at least one of these roles can vote:\n{", ".join(p.roles)}')
|
||||
return
|
||||
|
||||
# order here is crucial since we can't determine if a reaction was removed by the bot or user
|
||||
# update database with vote
|
||||
await p.vote(user, reaction.emoji, reaction.message)
|
||||
|
||||
# check if we need to remove reactions (this will trigger on_reaction_remove)
|
||||
if str(reaction.message.channel.type) != 'private':
|
||||
if p.anonymous:
|
||||
# immediately remove reaction
|
||||
await self.bot.remove_reaction(reaction.message, reaction.emoji, user)
|
||||
elif not p.multiple_choice:
|
||||
# remove all other reactions
|
||||
for r in reaction.message.reactions:
|
||||
if r != reaction:
|
||||
await self.bot.remove_reaction(reaction.message, r.emoji, user)
|
||||
|
||||
|
||||
|
||||
async def on_reaction_remove(self, reaction, user):
|
||||
if reaction.emoji.startswith(('⏪', '⏩')):
|
||||
return
|
||||
|
||||
# only look at our polls
|
||||
try:
|
||||
short = reaction.message.embeds[0]['author']['name'][3:]
|
||||
if not reaction.message.embeds[0]['author']['name'].startswith('>> ') or not short:
|
||||
return
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
# create message object for the reaction
|
||||
user_msg = copy.deepcopy(reaction.message)
|
||||
user_msg.author = user
|
||||
|
||||
server = await ask_for_server(self.bot, user_msg, short)
|
||||
if str(user_msg.channel.type) == 'private':
|
||||
user = server.get_member(user.id)
|
||||
user_msg.author = user
|
||||
|
||||
# fetch poll
|
||||
p = await Poll.load_from_db(self.bot, server.id, short)
|
||||
if p is None:
|
||||
return
|
||||
if not p.anonymous:
|
||||
# for anonymous polls we can't unvote because we need to hide reactions
|
||||
await p.unvote(user, reaction.emoji, reaction.message)
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(PollControls(bot))
|
||||
136
cogs/utils.py
Normal file
136
cogs/utils.py
Normal file
@ -0,0 +1,136 @@
|
||||
import discord
|
||||
|
||||
class Settings:
|
||||
def __init__(self):
|
||||
self.color = discord.Colour(int('7289da', 16))
|
||||
self.title_icon = "http://mnadler.ch/img/donat-chart-32.png"
|
||||
|
||||
SETTINGS = Settings()
|
||||
|
||||
async def get_pre(bot, message):
|
||||
'''Gets the prefix for a message.'''
|
||||
if str(message.channel.type) == 'private':
|
||||
shared_server_list = await get_servers(bot, message)
|
||||
if shared_server_list.__len__() == 0:
|
||||
return '!'
|
||||
elif shared_server_list.__len__() == 1:
|
||||
return await get_server_pre(bot, shared_server_list[0])
|
||||
else:
|
||||
# return a tuple of all prefixes.. this will check them all!
|
||||
return tuple([await get_server_pre(bot, s) for s in shared_server_list])
|
||||
else:
|
||||
return await get_server_pre(bot, message.server)
|
||||
|
||||
async def get_server_pre(bot, server):
|
||||
'''Gets the prefix for a server.'''
|
||||
try:
|
||||
result = await bot.db.config.find_one({'_id': str(server.id)})
|
||||
except AttributeError:
|
||||
return '!'
|
||||
if not result or not result.get('prefix'):
|
||||
return '!'
|
||||
return result.get('prefix')
|
||||
|
||||
|
||||
async def get_servers(bot, message, short=None):
|
||||
if message.server is None:
|
||||
|
||||
list_of_shared_servers = []
|
||||
for s in bot.servers:
|
||||
if message.author.id in [m.id for m in s.members]:
|
||||
list_of_shared_servers.append(s)
|
||||
|
||||
if short is not None:
|
||||
query = bot.db.polls.find({'short': short})
|
||||
if query is not None:
|
||||
server_ids_with_short = [poll['server_id'] async for poll in query]
|
||||
servers_with_short = [bot.get_server(x) for x in server_ids_with_short]
|
||||
shared_servers_with_short = list(set(servers_with_short).intersection(set(list_of_shared_servers)))
|
||||
if shared_servers_with_short.__len__() >= 1:
|
||||
return shared_servers_with_short
|
||||
|
||||
# do this if no shared server with short is found
|
||||
if list_of_shared_servers.__len__() == 0:
|
||||
return []
|
||||
else:
|
||||
return list_of_shared_servers
|
||||
|
||||
|
||||
else:
|
||||
return [message.server]
|
||||
|
||||
async def ask_for_server(bot, message, short=None):
|
||||
server_list = await get_servers(bot, message, short)
|
||||
if server_list.__len__() == 0:
|
||||
if short == None:
|
||||
await bot.say(
|
||||
'I could not find a common server where we can see eachother. If you think this is an error, please contact the developer.')
|
||||
else:
|
||||
await bot.say(f'I could not find a server where the poll {short} exists that we both can see.')
|
||||
return None
|
||||
elif server_list.__len__() == 1:
|
||||
return server_list[0]
|
||||
else:
|
||||
text = 'I\'m not sure which server you are referring to. Please tell me by typing the corresponding number.\n'
|
||||
i = 1
|
||||
for name in [s.name for s in server_list]:
|
||||
text += f'\n**{i}** - {name}'
|
||||
i += 1
|
||||
embed = discord.Embed(title="Select your server", description=text, color=SETTINGS.color)
|
||||
server_msg = await bot.send_message(message.channel, embed=embed)
|
||||
|
||||
valid_reply = False
|
||||
nr = 1
|
||||
while valid_reply == False:
|
||||
reply = await bot.wait_for_message(timeout=60, author=message.author)
|
||||
if reply and reply.content:
|
||||
if reply.content.startswith(await get_pre(bot, message)):
|
||||
# await bot.say('You can\'t use bot commands while I am waiting for an answer.'
|
||||
# '\n I\'ll stop waiting and execute your command.')
|
||||
return False
|
||||
if str(reply.content).isdigit():
|
||||
nr = int(reply.content)
|
||||
if 0 < nr <= server_list.__len__():
|
||||
valid_reply = True
|
||||
|
||||
return server_list[nr - 1]
|
||||
|
||||
async def ask_for_channel(bot, server, message):
|
||||
# if performed from a channel, return that channel
|
||||
if str(message.channel.type) == 'text':
|
||||
return message.channel
|
||||
|
||||
# if exactly 1 channel, return it
|
||||
channel_list = [c for c in server.channels if str(c.type) == 'text']
|
||||
if channel_list.__len__() == 1:
|
||||
return channel_list[0]
|
||||
|
||||
# if no channels, display error
|
||||
if channel_list.__len__() == 0:
|
||||
embed = discord.Embed(title="Select a channel", description='No text channels found on this server. Make sure I can see them.', color=SETTINGS.color)
|
||||
await bot.say(embed=embed)
|
||||
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
|
||||
for name in [c.name for c in channel_list]:
|
||||
text += f'\n**{i}** - {name}'
|
||||
i += 1
|
||||
embed = discord.Embed(title="Select a channel", description=text, color=SETTINGS.color)
|
||||
server_msg = await bot.say(embed=embed)
|
||||
|
||||
valid_reply = False
|
||||
nr = 1
|
||||
while valid_reply == False:
|
||||
reply = await bot.wait_for_message(timeout=60, author=message.author)
|
||||
if reply and reply.content:
|
||||
if reply.content.startswith(await get_pre(bot, message)):
|
||||
# await bot.say('You can\'t use bot commands while I am waiting for an answer.'
|
||||
# '\n I\'ll stop waiting and execute your command.')
|
||||
return False
|
||||
if str(reply.content).isdigit():
|
||||
nr = int(reply.content)
|
||||
if 0 < nr <= channel_list.__len__():
|
||||
valid_reply = True
|
||||
return channel_list[nr - 1]
|
||||
43
pollmaster.py
Normal file
43
pollmaster.py
Normal file
@ -0,0 +1,43 @@
|
||||
import os
|
||||
import aiohttp
|
||||
|
||||
|
||||
from discord.ext import commands
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
#os.environ['dbltoken'] = 'ABC' #for website..
|
||||
from cogs.utils import get_pre
|
||||
|
||||
os.environ['mongoDB'] = 'mongodb://localhost:27017/pollmaster'
|
||||
|
||||
# async def get_pre(bot, message):
|
||||
# '''Gets the prefix for the server.'''
|
||||
# print(str(message.content))
|
||||
# try:
|
||||
# result = await bot.db.config.find_one({'_id': str(message.server.id)})
|
||||
# except AttributeError:
|
||||
# return '!'
|
||||
# if not result or not result.get('prefix'):
|
||||
# return '!'
|
||||
# return result.get('prefix')
|
||||
|
||||
bot = commands.Bot(command_prefix=get_pre)
|
||||
dbltoken = os.environ.get('dbltoken')
|
||||
|
||||
extensions = ['cogs.config','cogs.poll_controls']
|
||||
for ext in extensions:
|
||||
bot.load_extension(ext)
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
|
||||
mongo = AsyncIOMotorClient(os.environ.get('mongodb'))
|
||||
bot.db = mongo.pollmaster
|
||||
bot.session = aiohttp.ClientSession()
|
||||
print(bot.db)
|
||||
# document = {'key': 'value'}
|
||||
# result = await bot.db.test_collection.insert_one(document)
|
||||
# print('result %s' % repr(result.inserted_id))
|
||||
|
||||
|
||||
bot.run('NDQ0ODMxNzIwNjU5ODc3ODg5.DdhqZw.fsicJ8FffOYn670uPGuC4giXIlk')
|
||||
4
settings.py
Normal file
4
settings.py
Normal file
@ -0,0 +1,4 @@
|
||||
import discord
|
||||
|
||||
|
||||
PURPLE = discord.Colour(int('7289da', 16))
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
14
utils/poll_name_generator.py
Normal file
14
utils/poll_name_generator.py
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user