added docker configs

This commit is contained in:
Hendrik Behme committed 2019-08-26 17:56:46 +02:00
1 parent a89f0df161
commit 5cb6bccd7f
28 files changed
+97 -13

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# IDE Folders
../.idea/
# PY VENV
venv/
# Tokens and Passwwords
essentials/secrets\.py
pollmaster\.log
cogs/__pycache__/
essentials/__pycache__/
utils/__pycache__/
# export
export/*
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Matthias Nadler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# Changelog for Version 2.2
## New features
- Polls will now automatically activate or close and post themselves to the specified channel
- Improved ❔ functionality: Now lists the current votes for each options
- pm!cmd feature is enabled again with more error logging
- Title and options now support most UTF-8 characters, meaning you can put emojis and special characters in your poll
## Changes and Fixes
- Improved performance and scalability. Should feel a lot more responsive now
- Fixed formatting issues for closed polls
- Export now shows server specific nickname is applicable
- Users can no longer create polls in channels where they don't have "send message" permissions
# 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
- Poll creation now interactive
- New prefix: **pm!** (can be customized)
- All commands and roles have been changed, check **pm!help**
## Complete Overhaul
- Voting is no longer done per text, but by using reactions
- Poll creation has been streamlined and is now interactive
- An exhaustive pm!help function
## New features
- Prepare polls in advance and schedule them or activate them on demand
- A quick poll function with default settings
- Multiple Choice as new settings (allow more than one answer)
## Full Private Message and Multi Server Support
- **Every command can be used in private messages with pollmaster**
- This works even if you are in many servers with the bot
- Context sensitive detection of which server(s) are affected
- Promt the user for a server if still ambiguous
## New Look
- Complete visual overhaul for every aspect
- New custom icons
## New Database
- Changed the way all the polls are stored
- Added server specific configurations
## New Prefix
- **pm!** is the new default prefix
- Prefix can be customized for each server
## New roles and permissions
- Users with "Manage Server" permissions can now use all functions regardless of pollmaster specific permissions
- Two new type of roles: *polladmin* and *polluser*
- They can be set with *!polladmin role* and *!polluser role*
- More infos pm!help -> configuration
- The *pollmaster* role is deprecated
## Compability
- Most databases will be preserved and converted to the new format
- If your server lost data, please join the support server or contact the developer
View File
Whitespace-only changes.
+57
View File
@@ -0,0 +1,57 @@
import logging
from discord.ext import commands
class Admin(commands.Cog):
def __init__(self, bot):
self.bot = bot
# every commands needs owner permissions
async def cog_check(self, ctx):
return self.bot.owner == ctx.author
async def cog_command_error(self, ctx, error):
if isinstance(error, commands.CheckFailure):
await ctx.send("Only the owner can use this module. Join the support discord server if you are having "
"any problems. This usage has been logged.")
logger.warning(f'User {ctx.author} ({ctx.author.id}) has tried to access a restricted '
f'command via {ctx.message.content}.')
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send("Missing a required argument for this command.")
else:
logger.warning(error)
@commands.command(aliases=['r'])
async def reload(self, ctx, *, cog):
logger.info(f'Trying to reload cog: cogs.{cog}.')
reply = ''
try:
self.bot.reload_extension('cogs.'+cog)
reply = f'Extension "cogs.{cog}" successfully reloaded.'
except commands.ExtensionNotFound:
reply = f'Extension "cogs.{cog}" not found.'
except commands.NoEntryPointError:
reply = f'Extension "cogs.{cog}" is missing a setup function.'
except commands.ExtensionFailed:
reply = f'Extension "cogs.{cog}" failed to start.'
except commands.ExtensionNotLoaded:
reply = f'Extension "cogs.{cog}" is not loaded... trying to load it. '
try:
self.bot.load_extension('cogs.'+cog)
except commands.ExtensionAlreadyLoaded:
reply += f'Could not load or reload extension since it is already loaded...'
except commands.ExtensionNotFound:
reply += f'Extension "cogs.{cog}" not found.'
except commands.ExtensionFailed:
reply = f'Extension "cogs.{cog}" failed to start.'
finally:
logger.info(reply)
await ctx.send(reply)
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(Admin(bot))
+71
View File
@@ -0,0 +1,71 @@
import logging
from discord.ext import commands
class Config(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.has_permissions(manage_guild=True)
async def prefix(self, ctx, *, pre):
'''Set a custom prefix for the server.'''
server = ctx.message.guild
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)
self.bot.pre[str(server.id)] = str(pre)
await ctx.send(msg)
@commands.command()
@commands.has_permissions(manage_guild=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.guild
if not role:
result = await self.bot.db.config.find_one({'_id': str(server.id)})
if result and result.get('admin_role'):
await ctx.send(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 ctx.send(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 ctx.send(f'Server role `{role}` can now manage all polls.')
else:
await ctx.send(f'Server role `{role}` not found.')
@commands.command()
@commands.has_permissions(manage_guild=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.guild
if not role:
result = await self.bot.db.config.find_one({'_id': str(server.id)})
if result and result.get('user_role'):
await ctx.send(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 ctx.send(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 ctx.send(f'Server role `{role}` can now create and manage their own polls.')
else:
await ctx.send(f'Server role `{role}` not found.')
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(Config(bot))
+40
View File
@@ -0,0 +1,40 @@
import dbl
import asyncio
import logging
from discord.ext import commands
from essentials.settings import SETTINGS
class DiscordBotsOrgAPI(commands.Cog):
"""Handles interactions with the discordbots.org API"""
def __init__(self, bot):
self.bot = bot
self.token = SETTINGS.dbl_token
self.dblpy = dbl.Client(self.bot, self.token)
self.bot.loop.create_task(self.update_stats())
async def update_stats(self):
"""This function runs every 30 minutes to automatically update your server count"""
while True:
logger.info('attempting to post server count')
try:
if SETTINGS.mode == 'production':
await self.dblpy.post_server_count()
logger.info('posted server count ({})'.format(len(self.bot.guilds)))
sum_users = 0
for guild in self.bot.guilds:
sum_users += len(guild.members)
logger.info(f'total users served by the bot: {sum_users}')
except Exception as e:
logger.exception('Failed to post server count\n{}: {}'.format(type(e).__name__, e))
await asyncio.sleep(1800)
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(DiscordBotsOrgAPI(bot))
+352
View File
@@ -0,0 +1,352 @@
import asyncio
import logging
import discord
from discord.ext import commands
from essentials.multi_server import get_server_pre, ask_for_server
from essentials.settings import SETTINGS
class Help(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.pages = ['🏠', '🆕', '🔍', '🕹', '🛠', '❔', '💖']
async def embed_list_reaction_handler(self, ctx, page, pre, msg=None):
embed = self.get_help_embed(page, pre)
if msg is None:
msg = await ctx.send(embed=embed)
# add reactions
for emoji in self.pages:
await msg.add_reaction(emoji)
else:
await msg.edit(embed=embed)
# wait for reactions (3 minutes)
def check(rct, usr):
return True if usr != self.bot.user and str(rct.emoji) in self.pages and rct.message.id == msg.id else False
try:
reaction, user = await self.bot.wait_for('reaction_add', timeout=300, check=check)
except asyncio.TimeoutError:
await msg.delete()
return None
else:
if isinstance(reaction.message.channel, discord.TextChannel):
await reaction.message.remove_reaction(reaction.emoji, user)
return reaction
def get_help_embed(self, page, pre):
title = f' Pollmaster Help - React with an emoji to learn more about a topic!'
embed = discord.Embed(title='', description='', colour=SETTINGS.color)
embed.set_author(name=title, icon_url=SETTINGS.author_icon)
embed.set_footer(text='Use reactions to navigate the help. This message will self-destruct in 5 minutes.')
if page == '🏠':
# POLL CREATION SHORT
embed.add_field(name='🆕 Making New Polls',
value=f'`{pre}quick` | `{pre}new` | `{pre}advanced` | `{pre}prepare` | `{pre}cmd <args>`',
inline=False)
# embed.add_field(name='Commands', value=f'`{pre}quick` | `{pre}new` | `{pre}prepared`', inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: `<poll question>` (optional)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}new` | `{pre}quick What is the greenest color?`',
# inline=False)
## POLL CONTROLS
embed.add_field(name='🔍 Show Polls',
value=f'`{pre}show` | `{pre}show <label>` | `{pre}show <category>`', inline=False)
# embed.add_field(name='Command', value=f'`{pre}show (label)`', inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: `open` (default) | `closed` | `prepared` | '
# f'`<poll_label>` (optional)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}show` | `{pre}show closed` | `{pre}show mascot`',
# inline=False)
# POLL CONTROLS
embed.add_field(name='🕹 Poll Controls',
value=f'`{pre}copy` | `{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ',
inline=False)
# embed.add_field(name='Commands', value=f'`{pre}close` | `{pre}export` | `{pre}delete` | `{pre}activate` ',
# inline=False)
# embed.add_field(name='Arguments', value=f'Arguments: <poll_label> (required)', inline=False)
# embed.add_field(name='Examples', value=f'Examples: `{pre}close mascot` | `{pre}export proposal`',
# inline=False)
# POLL CONTROLS
embed.add_field(name='🛠 Configuration',
value=f'`{pre}userrole [role]` | `{pre}adminrole [role]` | `{pre}prefix <new_prefix>` ',
inline=False
)
# DEBUGGING
embed.add_field(name='❔ Debugging',
value=f'`@debug` | `@mention` | `@mention <tag>` ',
inline=False
)
# ABOUT
embed.add_field(name='💖 About Pollmaster',
value='More infos about Pollmaster, the developer, where to go for further help and how you can support us.',
inline=False)
elif page == '🆕':
embed.add_field(name='🆕 Making New Polls',
value='There are four ways to create a new poll. For all the commands you can either just '
'type the command or type the command followed by the question to skip the first step.'
'Your Members need the <admin> or <user> role to use these commands. '
'More on user rights in 🛠 Configuration.',
inline=False)
embed.add_field(name=f'🔹 **Quick Poll:** `{pre}quick`',
value='If you just need a quick poll, this is the way to go. All you have to specify is the '
'question and your answers; the rest will be set to default values.',
inline=False)
embed.add_field(name=f'🔹 **Basic Poll:** `{pre}new`',
value='This command gives control over the most common settings. A step by step wizard will guide '
'you through the process and you can specify options such as Multiple Choice, '
'Anonymous Voting and Deadline.',
inline=False)
embed.add_field(name=f'🔹 **Advanced Poll:** `{pre}advanced`',
value='This command gives you full control over your poll. A step by step wizard will guide '
'you through the process and you can specify additional options such as Hide Vote Count, '
'Role Restrictions, Role Weights or Custom Write-In Answers (Survey Flags).',
inline=False)
embed.add_field(name=f'🔹 **Prepare and Schedule:** `{pre}prepare`',
value=f'Similar to `{pre}advanced`, this gives you all the options. But additionally, the poll will '
'be set to \'inactive\'. You can specify if the poll should activate at a certain time '
f'and/or if you would like to manually `{pre}activate` it. '
'Perfect if you are preparing for a team meeting!',
inline=False)
embed.add_field(name=f'🔹 **-Advanced- Commandline:** `{pre}cmd <arguments>`',
value=f'For the full syntax type `{pre}cmd help`\n'
f'Similar to version 1 of the bot, with this command you can create a poll in one message. '
f'Pass all the options you need via command line arguments, the rest will be set to '
f'default values. The wizard will step in for invalid arguments.\n'
f'Example: `{pre}cmd -q "Which colors?" -l colors -o "green, blue, red" -h -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. '
'Voting is done simply by using the reactions below the poll.',
inline=False)
embed.add_field(name=f'🔹 **Show a Poll:** `{pre}show <poll_label>`',
value='This command will refresh and display a poll. The votes in the message will always '
'be up to date and accurate. The number of reactions can be different for a number '
'of reasons and you can safely disregard them.',
inline=False)
embed.add_field(name=f'🔹 **List Polls:** `{pre}show <> | open | closed | prepared`',
value=f'If you just type `{pre}show` without an argument it will default to `{pre}show open`.'
'These commands will print a list of open, closed or prepared polls that exist on '
'the server. The first word in bold is the label of the poll and after the colon, '
'you can read the question. These lists are paginated and you can use the arrow '
'reactions to navigate larger lists.',
inline=False)
elif page == '🕹':
embed.add_field(name='🕹 Poll Controls',
value='All these commands except copy can only be used by an <admin> or by the author of the poll. '
'Go to 🛠 Configuration for more info on the permissions.',
inline=False)
embed.add_field(name=f'🔹 **Copy** `{pre}copy <poll_label>`',
value='This will give you a cmd string that you can post into any channel to create a copy'
'of the specified poll. It will increment the label and depending on the settings, '
'you might need to add missing information like a new deadline. '
f'\nFor more info, see: `{pre}cmd help`.',
inline=False)
embed.add_field(name=f'🔹 **Close** `{pre}close <poll_label>`',
value='Polls will close automatically when their deadline is reached. But you can always '
'close them manually by using this command. A closed poll will lock in the votes so '
'users can no longer change, add or remove votes. Once closed, you can export a poll.',
inline=False)
embed.add_field(name=f'🔹 **Delete** `{pre}delete <poll_label>`',
value='This will *permanently and irreversibly* delete a poll from the database. '
'Once done, the label is freed up and can be assigned again.',
inline=False)
embed.add_field(name=f'🔹 **Export** `{pre}export <poll_label>`',
value='You can use this command or react with 📎 to a closed poll to generate a report. '
'The report will then be sent to you in discord via the bot. This utf8-textfile '
'(make sure to open it in an utf8-ready editor) will contain all the infos about the '
'poll, including a detailed list of participants and their votes (just a list of names '
'for anonymous polls).',
inline=False)
embed.add_field(name=f'🔹 **Activate** `{pre}activate <poll_label>`',
value=f'To see how you can prepare inactive polls read the `{pre}prepare` command under Making '
'New Polls. This command is used to manually activate a prepared poll.',
inline=False)
elif page == '🛠':
embed.add_field(name='🛠 Configuration',
value='To run any of these commands you need the **\"Manage Server\"** permisson.',
inline=False)
embed.add_field(name=f'🔹 **Poll Admins** `{pre}adminrole <role name> (optional)`',
value='This gives the rights to create polls and to control ALL polls on the server. '
f'To see the current role for poll admin, run the command without an argument: `{pre}adminrole`\n'
'If you want to change the admin role to any other role, use the name of the new role '
f'as the argument: `{pre}adminrole moderators`',
inline=False)
embed.add_field(name=f'🔹 **Poll Users** `{pre}userrole <role name> (optional)`',
value='Everything here is identical to the admin role, except that Poll Users can only '
'control the polls which were created by themselves.',
inline=False)
embed.add_field(name=f'🔹 **Change Prefix** `{pre}prefix <new_prefix>`',
value='This will change the bot prefix for your server. If you want to use a trailing '
'whitespace, use "\w" instead of " " (discord deletes trailing whitespaces).',
inline=False)
elif page == '❔':
embed.add_field(name='❔ Debugging',
value='These commands are independent of your server prefix and serve to debug the bot.',
inline=False)
embed.add_field(name=f'🔹 **Debug:** `@debug`',
value='This command will check the required permissions in the channel it is used and'
'generate a short report with suggestions on your next actions.'
'If you are stuck, please visit the support discord server.',
inline=False)
embed.add_field(name=f'🔹 **Mention:** `@mention` | `@mention prefix`',
value='This is a prefix independent command to retrieve your prefix in case you changed '
'and forgot it. More `@mention` tags might be added in the future.',
inline=False)
elif page == '💖':
embed.add_field(name='💖 Pollmaster 💖',
value='If you enjoy the bot, you can show your appreciation by giving him an upvote on Discordbots.',
inline=False)
embed.add_field(name='🔹 **Developer**',
value='Pollmaster is developed by Newti#0654',
inline=False)
embed.add_field(name='🔹 **Support**',
value='You can support Pollmaster by sending an upvote his way or by clicking the donate link '
'on the discordbots page:\n https://discordbots.org/bot/444514223075360800',
inline=False)
embed.add_field(name='🔹 **Support Server**',
value='If you need help with pollmaster, want to try him out or would like to give feedback '
'to the developer, feel free to join the support server: https://discord.gg/Vgk8Nve',
inline=False)
embed.add_field(name='🔹 **Github**',
value='The full python source code is on my Github: https://github.com/matnad/pollmaster',
inline=False)
embed.add_field(name='**Thanks for using Pollmaster!** 💗', value='Newti', inline=False)
else:
return None
return embed
@commands.command()
async def help(self, ctx):
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
if not ctx.message.channel.permissions_for(server.me).embed_links:
await ctx.send("Missing permissions. Type \"@debug.\"")
return
pre = await get_server_pre(self.bot, server)
rct = 1
while rct is not None:
if rct == 1:
page = '🏠'
msg = None
else:
page = rct.emoji
msg = rct.message
rct = await self.embed_list_reaction_handler(ctx, page, pre, msg)
# cleanup
try:
await ctx.message.delete()
except PermissionError:
pass
# @mention and @debug commands
@commands.Cog.listener()
async def on_message(self, message):
if message.author == self.bot.user:
return
if message.content.startswith("@mention"):
channel = message.channel
if not isinstance(channel, discord.TextChannel):
await channel.send("@mention can only be used in a server text channel.")
return
guild = message.guild
if not guild:
await channel.send("Could not determine your server.")
return
if message.content == "@mention":
await channel.send("The following @mention tags are available:\n🔹 @mention prefix")
return
try:
tag = message.content.split()[1].lower()
except IndexError:
await channel.send("Wrong formatting. Type \"@mention\" or \"@mention <tag>\".")
return
if tag == "prefix":
pre = await get_server_pre(self.bot, guild)
# await channel.send(f'The prefix for this server/channel is: \n {pre} \n To change it type: \n'
# f'{pre}prefix <new_prefix>')
await channel.send(pre)
else:
await channel.send(f'Tag "{tag}" not found. Type "@mention" for a list of tags.')
elif message.content == "@debug":
channel = message.channel
if not isinstance(channel, discord.TextChannel):
await channel.send("@debug can only be used in a server text channel.")
return
guild = message.guild
if not guild:
await channel.send("Could not determine your server.")
return
status_msg = ''
setup_correct = True
# check send message permissions
permissions = channel.permissions_for(guild.me)
if not permissions.send_messages:
await message.author.send(f'I don\'t have permission to send text messages in channel "{channel}" '
f'on server "{guild}"')
return
status_msg += ' ✅ Sending text messages\n'
# check embed link permissions
if permissions.embed_links:
status_msg += '✅ Sending embedded messages\n'
else:
status_msg += '❗ Sending embedded messages. I need permissions to embed links!\n'
setup_correct = False
# check manage messages
if permissions.manage_messages:
status_msg += '✅ Deleting messages and reactions\n'
else:
status_msg += '❗ Deleting messages and reactions. I need the manage messages permission!\n'
setup_correct = False
# check adding reactions
if permissions.add_reactions:
status_msg += '✅ Adding reactions\n'
else:
status_msg += '❗ Adding reactions. I need the add reactions permission!\n'
setup_correct = False
if setup_correct:
status_msg += 'No action required. Your permissions are set up correctly for this channel. \n' \
'If the bot does not work, feel free to join the support discord server.'
else:
status_msg += 'Please try to fix the issues above. \nIf you are still having problems, ' \
'visit the support discord server.'
await channel.send(status_msg)
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(Help(bot))
+884
View File
@@ -0,0 +1,884 @@
import argparse
import os
import asyncio
import datetime
import logging
import re
import shlex
import discord
import pytz
import time
from discord.ext import commands
from utils.misc import CustomFormatter
from models.poll import Poll
from utils.paginator import embed_list_paginated
from essentials.multi_server import get_server_pre, ask_for_server, ask_for_channel
from essentials.settings import SETTINGS
from utils.poll_name_generator import generate_word
from essentials.exceptions import StopWizard
# A-Z Emojis for Discord
AZ_EMOJIS = [(b'\\U0001f1a'.replace(b'a', bytes(hex(224 + (6 + i))[2:], "utf-8"))).decode("unicode-escape") for i in
range(26)]
class PollControls(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.bot.loop.create_task(self.close_polls())
self.ignore_next_removed_reaction = {}
# # General Methods
def get_label(self, message: discord.Message):
label = None
if message and message.embeds:
embed = message.embeds[0]
label_object = embed.author
if label_object:
label_full = label_object.name
if label_full and label_full.startswith('>> '):
label = label_full[3:]
return label
async def close_polls(self):
"""This function runs every 60 seconds to schedule prepared polls and close expired polls"""
while True:
try:
if not hasattr(self.bot, 'db'):
"""await asyncio.sleep(30)"""
logger.info("DB not available yet")
time.sleep(30)
continue
query = self.bot.db.polls.find({'active': False, 'activation': {"$not": re.compile("0")}})
if query:
for pd in [poll async for poll in query]:
p = Poll(self.bot, load=True)
await p.from_dict(pd)
if not p.server:
continue
if p.active:
try:
await p.channel.send('This poll has been scheduled and is active now!')
await p.post_embed(p.channel)
except:
continue
query = self.bot.db.polls.find({'open': True, 'duration': {"$not": re.compile("0")}})
if query:
for pd in [poll async for poll in query]:
p = Poll(self.bot, load=True)
await p.from_dict(pd)
if not p.server:
continue
if not p.open:
try:
await p.channel.send('This poll has reached the deadline and is closed!')
await p.post_embed(p.channel)
except:
continue
except AttributeError as ae:
# Database not loaded yet
logger.warning("Attribute Error in close_polls loop")
logger.exception(ae)
pass
except Exception as ex:
# Never break this loop due to an error
logger.error("Other Error in close_polls loop")
logger.exception(ex)
pass
await asyncio.sleep(60)
def get_lock(self, server_id):
if not self.bot.locks.get(server_id):
self.bot.locks[server_id] = asyncio.Lock()
return self.bot.locks.get(server_id)
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
elif member.guild_permissions.manage_guild:
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 ctx.send(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.author_icon)
if footer_text is not None:
embed.set_footer(text=footer_text)
await ctx.send(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.author_icon)
if footer_text is not None:
embed.set_footer(text=footer_text)
await ctx.send(embed=embed)
# Commands
@commands.command()
async def activate(self, ctx, *, short=None):
"""Activate a prepared poll. Parameter: <label>"""
server = await ask_for_server(self.bot, ctx.message, short)
if not server:
return
if short is None:
pre = await get_server_pre(self.bot, ctx.message.guild)
error = f'Please specify the label of a poll after the activate command. \n' \
f'`{pre}activate <poll_label>`'
await self.say_error(ctx, error)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
# check if already active, then just do nothing
if await p.is_active():
return
# 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 activate this poll. Please talk to the server admin.'
):
return
# Activate Poll
p.active = True
await p.save_to_db()
await ctx.invoke(self.show, short)
else:
error = f'Poll with label "{short}" was not found. Listing prepared polls.'
# pre = await get_server_pre(self.bot, ctx.message.server)
# footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error)
await ctx.invoke(self.show, 'prepared')
@commands.command()
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.guild)
error = f'Please specify the label of a poll after the delete command. \n' \
f'`{pre}delete <poll_label>`'
await self.say_error(ctx, error)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
# 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': str(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.guild)
footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error, footer)
@commands.command()
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.guild)
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, 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. Listing all open polls.'
# pre = await get_server_pre(self.bot, ctx.message.server)
# footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error)
await ctx.invoke(self.show)
@commands.command()
async def copy(self, ctx, *, short=None):
'''Copy a poll. Parameter: <label>'''
server = await ask_for_server(self.bot, ctx.message, short)
if not server:
return
if short is None:
pre = await get_server_pre(self.bot, ctx.message.guild)
error = f'Please specify the label of a poll after the copy command. \n' \
f'`{pre}copy <poll_label>`'
await self.say_error(ctx, error)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
text = await get_server_pre(self.bot, server) + p.to_command()
await self.say_embed(ctx, text, title="Paste this to create a copy of the poll")
else:
error = f'Poll with label "{short}" was not found. Listing all open polls.'
# pre = await get_server_pre(self.bot, ctx.message.server)
# footer = f'Type {pre}show to display all polls'
await self.say_error(ctx, error)
await ctx.invoke(self.show)
@commands.command()
async def export(self, ctx, *, short=None):
'''Export a poll. Parameter: <label>'''
server = await ask_for_server(self.bot, ctx.message, short)
if not server:
return
if short is None:
pre = await get_server_pre(self.bot, ctx.message.guild)
error = f'Please specify the label of a poll after the export command. \n' \
f'`{pre}export <poll_label>`'
await self.say_error(ctx, error)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
if p.open:
pre = await get_server_pre(self.bot, ctx.message.guild)
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_name = await p.export()
if file_name is not None:
await ctx.message.author.send('Sending you the requested export of "{}".'.format(p.short),
file=discord.File(file_name)
)
# await self.bot.send_file(
# ctx.message.author,
# file_name,
# 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()
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, 'active': True})
elif short == 'closed':
query = self.bot.db.polls.find({'server_id': str(server.id), 'open': False, 'active': True})
elif short == 'prepared':
query = self.bot.db.polls.find({'server_id': str(server.id), 'active': False})
if query is not None:
# sort by newest first
polls = [poll async for poll in query.sort('_id', -1)]
else:
return
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.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)
footer_text = f'type {pre}show <label> to display a poll. '
msg = await embed_list_paginated(ctx, self.bot, pre, polls, item_fct, embed, footer_prefix=footer_text,
per_page=10)
else:
p = await Poll.load_from_db(self.bot, server.id, short)
if p is not None:
error_msg = 'This poll is inactive and you have no rights to display or view it.'
if not await p.is_active() and not await self.is_admin_or_creator(ctx, server, p.author, error_msg):
return
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()
async def cmd(self, ctx, *, cmd=None):
'''The old, command style way paired with the wizard.'''
# await self.say_embed(ctx, say_text='This command is temporarily disabled.')
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
pre = await get_server_pre(self.bot, server)
try:
# generate the argparser and handle invalid stuff
descr = 'Accept poll settings via commandstring. \n\n' \
'**Wrap all arguments in quotes like this:** \n' \
f'{pre}cmd -question \"What tea do you like?\" -o \"green, black, chai\"\n\n' \
'The Order of arguments doesn\'t matter. If an argument is missing, it will use the default value. ' \
'If an argument is invalid, the wizard will step in. ' \
'If the command string is invalid, you will get this error :)'
parser = argparse.ArgumentParser(description=descr, formatter_class=CustomFormatter, add_help=False)
parser.add_argument('-question', '-q')
parser.add_argument('-label', '-l', default=str(await generate_word(self.bot, server.id)))
parser.add_argument('-anonymous', '-a', action="store_true")
parser.add_argument('-options', '-o')
parser.add_argument('-survey_flags', '-sf', default='0')
parser.add_argument('-multiple_choice', '-mc', default='1')
parser.add_argument('-hide_votes', '-h', action="store_true")
parser.add_argument('-roles', '-r', default='all')
parser.add_argument('-weights', '-w', default='none')
parser.add_argument('-prepare', '-p', default='-1')
parser.add_argument('-deadline', '-d', default='0')
helpstring = parser.format_help()
helpstring = helpstring.replace("pollmaster.py", f"{pre}cmd ")
if not cmd or len(cmd) < 2 or cmd == 'help':
# Shlex will block if the string is empty
await self.say_embed(ctx, say_text=helpstring)
return
try:
print(cmd)
cmd = cmd.replace('“', '"') # fix for iphone keyboard
cmd = cmd.replace('”', '"') # fix for iphone keyboard
print(cmd)
cmds = shlex.split(cmd)
except ValueError:
await self.say_error(ctx, error_text=helpstring)
return
except:
return
try:
args, unknown_args = parser.parse_known_args(cmds)
except SystemExit:
await self.say_error(ctx, error_text=helpstring)
return
except:
return
if unknown_args:
error_text = f'**There was an error reading the command line options!**.\n' \
f'Most likely this is because you didn\'t surround the arguments with double quotes like this: ' \
f'`{pre}cmd -q "question of the poll" -o "yes, no, maybe"`' \
f'\n\nHere are the arguments I could not understand:\n'
error_text += '`'+'\n'.join(unknown_args)+'`'
error_text += f'\n\nHere are the arguments which are ok:\n'
error_text += '`' + '\n'.join([f'{k}: {v}' for k, v in vars(args).items()]) + '`'
await self.say_error(ctx, error_text=error_text, footer_text=f'type `{pre}cmd help` for details.')
return
# pass arguments to the wizard
async def route(poll):
await poll.set_name(ctx, force=args.question)
await poll.set_short(ctx, force=args.label)
await poll.set_anonymous(ctx, force=f'{"yes" if args.anonymous else "no"}')
await poll.set_options_reaction(ctx, force=args.options)
await poll.set_survey_flags(ctx, force=args.survey_flags)
await poll.set_multiple_choice(ctx, force=args.multiple_choice)
await poll.set_hide_vote_count(ctx, force=f'{"yes" if args.hide_votes else "no"}')
await poll.set_roles(ctx, force=args.roles)
await poll.set_weights(ctx, force=args.weights)
await poll.set_preparation(ctx, force=args.prepare)
await poll.set_duration(ctx, force=args.deadline)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
except Exception as error:
logger.error("ERROR IN pm!cmd")
logger.exception(error)
@commands.command()
async def quick(self, ctx, *, cmd=None):
'''Create a quick poll with just a question and some options. Parameters: <Question> (optional)'''
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
async def route(poll):
await poll.set_name(ctx, force=cmd)
await poll.set_short(ctx, force=str(await generate_word(self.bot, server.id)))
await poll.set_anonymous(ctx, force='no')
await poll.set_options_reaction(ctx)
await poll.set_multiple_choice(ctx, force='1')
await poll.set_hide_vote_count(ctx, force='no')
await poll.set_roles(ctx, force='all')
await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx, force='0')
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
@commands.command()
async def prepare(self, ctx, *, cmd=None):
"""Prepare a poll to use later. Parameters: <Question> (optional)"""
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
async def route(poll):
await poll.set_name(ctx, force=cmd)
await poll.set_short(ctx)
await poll.set_preparation(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx)
await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx)
await poll.set_roles(ctx)
await poll.set_weights(ctx)
await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(ctx.message.author)
@commands.command()
async def advanced(self, ctx, *, cmd=None):
"""Poll with more options. Parameters: <Question> (optional)"""
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
async def route(poll):
await poll.set_name(ctx, force=cmd)
await poll.set_short(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx)
await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx)
await poll.set_roles(ctx)
await poll.set_weights(ctx)
await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
@commands.command()
async def new(self, ctx, *, cmd=None):
"""Start the poll wizard to create a new poll step by step. Parameters: <Question> (optional)"""
server = await ask_for_server(self.bot, ctx.message)
if not server:
return
async def route(poll):
await poll.set_name(ctx, force=cmd)
await poll.set_short(ctx)
await poll.set_anonymous(ctx)
await poll.set_options_reaction(ctx)
await poll.set_survey_flags(ctx, force='0')
await poll.set_multiple_choice(ctx)
await poll.set_hide_vote_count(ctx, force='no')
await poll.set_roles(ctx, force='all')
await poll.set_weights(ctx, force='none')
await poll.set_duration(ctx)
poll = await self.wizard(ctx, route, server)
if poll:
await poll.post_embed(poll.channel)
# The Wizard!
async def wizard(self, ctx, route, server):
channel = await ask_for_channel(ctx, self.bot, server, ctx.message)
if not channel:
return
pre = await get_server_pre(self.bot, server)
# Permission Check
member = server.get_member(ctx.message.author.id)
if not member.guild_permissions.manage_guild:
result = await self.bot.db.config.find_one({'_id': str(server.id)})
if result and result.get('admin_role') not in [r.name for r in member.roles] and result.get(
'user_role') not in [r.name for r in member.roles]:
await ctx.message.author.send('You don\'t have sufficient rights to start new polls on this server. '
'A server administrator has to assign the user or admin role to you. '
f'To view and set the permissions, an admin can use `{pre}userrole` and '
f'`{pre}adminrole`')
return
## Create object
poll = Poll(self.bot, ctx, server, channel)
## Route to define object, passed as argument for different constructors
if ctx.message and ctx.message.content and not ctx.message.content.startswith(f'{pre}cmd '):
poll.wizard_messages.append(ctx.message)
try:
await route(poll)
poll.finalize()
await poll.clean_up(ctx.channel)
except StopWizard:
await poll.clean_up(ctx.channel)
return
# Finalize
await poll.save_to_db()
return poll
@commands.Cog.listener()
async def on_raw_reaction_remove(self, data):
# get emoji symbol
emoji = data.emoji
if emoji:
emoji = emoji.name
if not emoji:
return
# check if removed by the bot.. this is a bit hacky but discord doesn't provide the correct info...
message_id = data.message_id
user_id = data.user_id
if self.ignore_next_removed_reaction.get(str(message_id) + str(emoji)) == user_id:
del self.ignore_next_removed_reaction[str(message_id) + str(emoji)]
return
# check if we can find a poll label
message_id = data.message_id
channel_id = data.channel_id
channel = self.bot.get_channel(channel_id)
if isinstance(channel, discord.TextChannel):
server = channel.guild
user = server.get_member(user_id)
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
elif isinstance(channel, discord.DMChannel):
user = await self.bot.fetch_user(user_id) # only do this once
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
server = await ask_for_server(self.bot, message, label)
elif not channel:
# discord rapidly closes dm channels by desing
# put private channels back into the bots cache and try again
user = await self.bot.fetch_user(user_id) # only do this once
await user.create_dm()
channel = self.bot.get_channel(channel_id)
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
server = await ask_for_server(self.bot, message, label)
else:
return
# this is exclusive
lock = self.get_lock(server.id)
async with lock:
p = await Poll.load_from_db(self.bot, server.id, label)
if not isinstance(p, Poll):
return
if not p.anonymous:
# for anonymous polls we can't unvote because we need to hide reactions
await p.unvote(user, emoji, message, lock)
@commands.Cog.listener()
async def on_raw_reaction_add(self, data):
# dont look at bot's own reactions
user_id = data.user_id
if user_id == self.bot.user.id:
return
# get emoji symbol
emoji = data.emoji
if emoji:
emoji = emoji.name
if not emoji:
return
# check if we can find a poll label
message_id = data.message_id
channel_id = data.channel_id
channel = self.bot.get_channel(channel_id)
if isinstance(channel, discord.TextChannel):
server = channel.guild
user = server.get_member(user_id)
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
elif isinstance(channel, discord.DMChannel):
user = await self.bot.fetch_user(user_id) # only do this once
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
server = await ask_for_server(self.bot, message, label)
elif not channel:
# discord rapidly closes dm channels by desing
# put private channels back into the bots cache and try again
user = await self.bot.fetch_user(user_id) # only do this once
await user.create_dm()
channel = self.bot.get_channel(channel_id)
message = self.bot.message_cache.get(message_id)
if message is None:
message = await channel.fetch_message(id=message_id)
self.bot.message_cache.put(message_id, message)
label = self.get_label(message)
if not label:
return
server = await ask_for_server(self.bot, message, label)
else:
return
# fetch poll
# create message object for the reaction sender, to get correct server
# user_msg = copy.deepcopy(message)
# user_msg.author = user
# server = await ask_for_server(self.bot, user_msg, label)
# this is exclusive to keep database access sequential
# hopefully it will scale well enough or I need a different solution
lock = self.get_lock(server.id)
async with lock:
p = await Poll.load_from_db(self.bot, server.id, label)
if not isinstance(p, Poll):
return
# export
if emoji == '📎':
# sending file
file_name = await p.export()
if file_name is not None:
await user.send('Sending you the requested export of "{}".'.format(p.short),
file=discord.File(file_name)
)
return
# 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)
# created by
created_by = server.get_member(int(p.author.id))
embed.add_field(name=f'Created by:', value=f'{created_by if created_by else "<Deleted User>"}', inline=False)
# vote rights
vote_rights = await p.has_required_role(member)
embed.add_field(name=f'{"Can you vote?" if is_open else "Could you vote?"}',
value=f'{"✅" if vote_rights else "❎"}', inline=False)
# edit rights
edit_rights = False
if str(member.id) == str(p.author.id):
edit_rights = True
elif member.guild_permissions.manage_guild:
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 str(user.id) in p.votes:
if p.votes[str(user.id)]['choices'].__len__() > 0:
choices = ', '.join([p.options_reaction[c] for c in p.votes[str(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 user.send(embed=embed)
# send current details of who currently voted for what
if (not p.open or not p.hide_count) and not p.anonymous and p.votes.__len__() > 0:
msg = '--------------------------------------------\n' \
'CURRENT VOTES\n' \
'--------------------------------------------\n'
for i, o in enumerate(p.options_reaction):
if not p.options_reaction_default:
msg += AZ_EMOJIS[i] + " "
msg += "**" +o+":**"
c = 0
for user_id in p.votes:
member = server.get_member(int(user_id))
if not member or i not in p.votes[str(user_id)]['choices']:
continue
c += 1
name = member.nick
if not name:
name = member.name
if not name:
name = "<Deleted User>"
msg += f'\n{name}'
if p.votes[str(user_id)]['weight'] != 1:
msg += f' (weight: {p.votes[str(user_id)]["weight"]})'
# msg += ': ' + ', '.join([AZ_EMOJIS[c]+" "+p.options_reaction[c] for c in p.votes[user_id]['choices']])
if i in p.survey_flags:
msg += f': {p.votes[str(user_id)]["answers"][p.survey_flags.index(i)]}'
if msg.__len__() > 1500:
await user.send(msg)
msg = ''
if c == 0:
msg += '\nNo votes for this option yet.'
msg += '\n\n'
if msg.__len__() > 0:
await user.send(msg)
elif (not p.open or not p.hide_count) and p.anonymous and p.survey_flags.__len__() > 0 and p.votes.__len__() > 0:
msg = '--------------------------------------------\n' \
'Custom Answers (Anonymous)\n' \
'--------------------------------------------\n'
has_answers = False
for i, o in enumerate(p.options_reaction):
if i not in p.survey_flags:
continue
custom_answers = ''
for user_id in p.votes:
if i in p.votes[str(user_id)]["choices"]:
has_answers = True
custom_answers += f'\n{p.votes[str(user_id)]["answers"][p.survey_flags.index(i)]}'
if custom_answers.__len__() > 0:
msg += AZ_EMOJIS[i] + " "
msg += "**" + o + ":**"
msg += custom_answers
msg += '\n\n'
if msg.__len__() > 1500:
await user.send(msg)
msg = ''
if has_answers and msg.__len__() > 0:
await user.send(msg)
return
# Assume: User wants to vote with reaction
# no rights, terminate function
if not await p.has_required_role(member):
await message.remove_reaction(emoji, user)
await member.send(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
# check if we need to remove reactions (this will trigger on_reaction_remove)
if not isinstance(channel, discord.DMChannel) and p.anonymous:
# immediately remove reaction and to be safe, remove all reactions
self.ignore_next_removed_reaction[str(message.id) + str(emoji)] = user_id
asyncio.ensure_future(message.remove_reaction(emoji, user))
# order here is crucial since we can't determine if a reaction was removed by the bot or user
# update database with vote
await p.vote(member, emoji, message, lock)
# cant do this until we figure out how to see who removed the reaction?
# for now MC 1 is like MC x
# if isinstance(channel, discord.TextChannel) and p.multiple_choice == 1:
# # remove all other reactions
# # if lock._waiters.__len__() == 0:
# for r in message.reactions:
# if r.emoji and r.emoji != emoji:
# await message.remove_reaction(r.emoji, user)
# pass
def setup(bot):
global logger
logger = logging.getLogger('bot')
bot.add_cog(PollControls(bot))
View File
Whitespace-only changes.
+48
View File
@@ -0,0 +1,48 @@
"""Exception Classes for the Poll Wizard"""
class StopWizard(RuntimeError):
pass
class InputError(RuntimeError):
pass
class InvalidInput(InputError):
pass
class ReservedInput(InputError):
pass
class DuplicateInput(InputError):
pass
class WrongNumberOfArguments(InputError):
pass
class ExpectedInteger(InputError):
pass
class ExpectedSeparator(InputError):
def __init__(self, separator):
self.separator = separator
class OutOfRange(InputError):
pass
class DateOutOfRange(InputError):
def __init__(self, date):
self.date = date
class InvalidRoles(InputError):
def __init__(self, roles):
self.roles = roles
+27
View File
@@ -0,0 +1,27 @@
import logging
import discord
logger = logging.getLogger('bot')
class MessageCache:
def __init__(self, _bot):
self._bot = _bot
self._cache_dict = {}
def put(self, key, value: discord.Message):
self._cache_dict[key] = value
if self._cache_dict.__len__() % 5 == 0:
logger.info("cache size: " + str(self._cache_dict.__len__()))
def get(self, key):
# Try to find it in this cache, then see if it is cached in the bots own message cache
message = self._cache_dict.get(key, None)
if message == None:
for m in self._bot._connection._messages:
if m.id == key:
return m
return message
def clear(self):
self._cache_dict = {}
+164
View File
@@ -0,0 +1,164 @@
import asyncio
import discord
from essentials.settings import SETTINGS
async def get_pre(bot, message):
'''Gets the prefix for a message.'''
if isinstance(message.channel, discord.abc.PrivateChannel):
shared_server_list = await get_servers(bot, message)
if shared_server_list.__len__() == 0:
return 'pm!'
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.guild)
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)})
result = bot.pre[str(server.id)]
except AttributeError:
return 'pm!'
if not result: #or not result.get('prefix'):
return 'pm!'
return result #result.get('prefix')
async def get_servers(bot, message, short=None):
"""Get best guess of relevant shared servers"""
if message.guild is None:
list_of_shared_servers = []
for s in bot.guilds:
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_guild(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.guild]
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 message.channel.send(embed=embed)
valid_reply = False
nr = 1
while valid_reply == False:
def check(m):
return message.author == m.author
try:
reply = await bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError:
pass
else:
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(ctx, bot, server, message):
# if performed from a channel, return that channel
if not isinstance(message.channel, discord.abc.PrivateChannel):
return message.channel
# build channel list that the user is allowed to send messages to
user = message.author
member = server.get_member(user.id)
channel_list = [c for c in server.channels if isinstance(c, discord.TextChannel) and c.permissions_for(member).send_messages]
# if exactly 1 channel, return it
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 ctx.send(embed=embed)
return False
# otherwise ask for a channel
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]:
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 ctx.send(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)
await ctx.send(embed=embed)
valid_reply = False
nr = 1
while not valid_reply:
def check(m):
return message.author.id == m.author.id
try:
reply = await bot.wait_for('message', timeout=60, check=check)
except asyncio.TimeoutError:
pass
else:
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]
+29
View File
@@ -0,0 +1,29 @@
import discord
import os
from essentials.secrets import SECRETS
class Settings:
def __init__(self):
self.color = discord.Colour(int('7289da', 16))
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 = os.environ.get('OWNER_ID')
self.msg_errors = True
self.log_errors = True
self.invite_link = \
'https://discordapp.com/api/oauth2/authorize?client_id=%s&permissions=126016&scope=bot' % (os.environ.get('INVITE_CLIENT_ID'))
self.load_secrets()
def load_secrets(self):
# secret
self.dbl_token = SECRETS.dbl_token
self.mongo_db = SECRETS.mongo_db
self.bot_token = SECRETS.bot_token
self.mode = SECRETS.mode
SETTINGS = Settings()
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+1511
View File
File diff suppressed because it is too large. Load diff
+143
View File
@@ -0,0 +1,143 @@
import sys
import traceback
import aiohttp
import discord
import logging
from essentials.messagecache import MessageCache
from discord.ext import commands
from motor.motor_asyncio import AsyncIOMotorClient
from essentials.multi_server import get_pre
from essentials.settings import SETTINGS
bot_config = {
'command_prefix': get_pre,
'pm_help': False,
'status': discord.Status.online,
'owner_id': SETTINGS.owner_id,
'fetch_offline_members': False,
'max_messages': 15000
}
bot = commands.AutoShardedBot(**bot_config)
bot.remove_command('help')
# logger
# create logger with 'spam_application'
logger = logging.getLogger('bot')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('pollmaster.log')
fh.setLevel(logging.DEBUG)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# add the handlers to the logger
logger.addHandler(fh)
logger.addHandler(ch)
@bot.event
async def on_ready():
bot.owner = await bot.fetch_user(SETTINGS.owner_id)
bot.session = aiohttp.ClientSession()
print(bot.db)
# check discord server configs
try:
db_server_ids = [entry['_id'] async for entry in bot.db.config.find({}, {})]
for server in bot.guilds:
if str(server.id) not in db_server_ids:
# create new config entry
await bot.db.config.update_one(
{'_id': str(server.id)},
{'$set': {'prefix': 'pm!', 'admin_role': 'polladmin', 'user_role': 'polluser'}},
upsert=True
)
except:
print("Problem verifying servers.")
logger.error("Problem verifying servers.")
# cache prefixes
bot.pre = {entry['_id']: entry['prefix'] async for entry in bot.db.config.find({}, {'_id', 'prefix'})}
bot.locks = {}
bot.message_cache = MessageCache(bot)
game = discord.Game("Democracy 4")
await bot.change_presence(status=discord.Status.online, activity=game)
print("Servers verified. Bot running.")
logger.info("Servers verified. Bot running.")
@bot.event
async def on_command_error(ctx, e):
if ctx.cog.qualified_name == "Admin":
# Admin cog handles the errors locally
return
if SETTINGS.log_errors:
ignored_exceptions = (
commands.MissingRequiredArgument,
commands.CommandNotFound,
commands.DisabledCommand,
commands.BadArgument,
commands.NoPrivateMessage,
commands.CheckFailure,
commands.CommandOnCooldown,
)
if isinstance(e, ignored_exceptions):
# log warnings
# logger.warning(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}')
return
# log error
logger.error(f'{type(e).__name__}: {e}\n{"".join(traceback.format_tb(e.__traceback__))}')
traceback.print_exception(type(e), e, e.__traceback__, file=sys.stderr)
if SETTINGS.msg_errors:
# send discord message for unexpected errors
e = discord.Embed(
title=f"Error With command: {ctx.command.name}",
description=f"```py\n{type(e).__name__}: {str(e)}\n```\n\nContent:{ctx.message.content}"
f"\n\tServer: {ctx.message.server}\n\tChannel: <#{ctx.message.channel}>"
f"\n\tAuthor: <@{ctx.message.author}>",
timestamp=ctx.message.timestamp
)
await ctx.send(bot.owner, embed=e)
# if SETTINGS.mode == 'development':
raise e
@bot.event
async def on_guild_join(server):
result = await bot.db.config.find_one({'_id': str(server.id)})
if result is None:
await bot.db.config.update_one(
{'_id': str(server.id)},
{'$set': {'prefix': 'pm!', 'admin_role': 'polladmin', 'user_role': 'polluser'}},
upsert=True
)
bot.pre[str(server.id)] = 'pm!'
logger.info("Connecting to Mongo DB: " + SETTINGS.mongo_db)
mongo = AsyncIOMotorClient(SETTINGS.mongo_db)
bot.db = mongo.pollmaster
print(bot.db)
extensions = ['cogs.config', 'cogs.poll_controls', 'cogs.help', 'cogs.db_api', 'cogs.admin']
for ext in extensions:
logger.info("Loading extension " + ext)
bot.load_extension(ext)
bot.run(SETTINGS.bot_token)
+56
View File
@@ -0,0 +1,56 @@
# Pollmaster V 2.4
## Overview
With Pollmaster you can very easily create and manage complex polls on your server.
Here is a quick list of features:
- Voting works with reactions (users don't need to type anything)
- Anonymous voting is possible
- You can hide the current vote count to prevent sheeping
- Polls can be single choice, multiple choice or restricted to a specific number of choices
- You can prepare polls in advance and schedule them to a date and time or manually activate them
- Polls can be given a deadline or they can be open until closed manually
- You can restrict which roles are allowed to participate
- You can assign weights to roles to count their votes more or less
- Polls are persistent and will be available until you delete them
- Personalised info for each poll by reacting with ❔
- Everything can be done in private chat with the bot to reduce spam on the server
- Full multi-server support!
- For advanced users: Commandline support to create a full poll in one line -> pm!cmd help
## Take a look!
Here is how Pollmaster looks in action:
![Pollmaster in action](https://i.imgur.com/C3zqnK2.png "Poll 1")
![Pollmaster in action](https://i.imgur.com/an0E3EO.png "Poll 2")
## The most important commands
| Command | Effect |
|------------------------|----------------------------------------------------|
| pm!help | Shows an interactive help menu |
| pm!new | Starts a new poll with the most common settings |
| pm!advanced | Starts a new poll all the settings |
| pm!quick | Starts a new poll with just a question and options |
| pm!show <label> | Shows poll in a specified channel (can be different from original channel) |
| pm!prefix <new prefix> | Change the prefix for this server |
| @mention prefix | Show the prefix if you forgot it |
| pm!userrole <any role> | Set the role that has the rights to use the bot |
## Getting Started
Users with the server permission "Manage Server" will have all rights from the start, meaning you can get started with pm!new right away!
To grant users without "Manage Server" poll creation access to the bot, create and distribute the role *polladmin* or *polluser*. If you already have roles for these rights you can tell the bot by typing *pm!adminrole <your role>* and *pm!userrole <your role>*.
## Need help? Want to try out Pollmaster?
Join the support discord server by clicking the button on the top of the page.
[![Discord Bots](https://discordbots.org/api/widget/444514223075360800.svg)](https://discordbots.org/bot/444514223075360800)
+27
View File
@@ -0,0 +1,27 @@
aiohttp==3.5.4
async-timeout==3.0.1
attrs==19.1.0
chardet==3.0.4
cycler==0.10.0
dateparser==0.7.1
dblpy==0.1.6
discord.py==1.0.1
idna==2.8
idna-ssl==1.1.0
kiwisolver==1.0.1
matplotlib==3.0.3
motor==2.0.0
multidict==4.5.2
numpy==1.16.2
pymongo==3.7.2
pyparsing==2.4.0
python-dateutil==2.8.0
pytz==2019.1
ratelimiter==1.2.0.post0
regex==2019.4.10
six==1.12.0
typing-extensions==3.7.2
tzlocal==1.5.1
Unidecode==1.0.23
websockets==6.0
yarl==1.3.0
View File
Whitespace-only changes.
+53
View File
@@ -0,0 +1,53 @@
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
# convert the float hours offset to a timedelta
offset_days, offset_seconds = 0, int(tz_offset * 3600)
if offset_seconds < 0:
offset_days = -1
offset_seconds += 24 * 3600
desired_delta = dt.timedelta(offset_days, offset_seconds)
# Loop through the timezones and find any with matching offsets
null_delta = dt.timedelta(0, 0)
results = []
for tz_name in timezones:
tz = pytz.timezone(tz_name)
non_dst_offset = getattr(tz, '_transition_info', [[null_delta]])[-1]
if desired_delta == non_dst_offset[0]:
results.append(tz_name)
return results
+55
View File
@@ -0,0 +1,55 @@
import asyncio
import discord
async def embed_list_paginated(ctx, bot, pre, items, item_fct, base_embed, footer_prefix='', msg=None, start=0, per_page=10):
embed = base_embed
# generate list
embed.title = f'{items.__len__()} entries'
text = '\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
#footer_text = f'Type {pre}show <label> to show a poll. '
footer_text = footer_prefix
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 msg.edit(embed=embed)
if not isinstance(msg.channel, discord.abc.PrivateChannel):
await msg.clear_reactions()
else:
msg = await ctx.send(embed=embed)
# add reactions
if start > 0:
await msg.add_reaction('⏪')
if items.__len__() > start+per_page:
await msg.add_reaction('⏩')
# wait for reactions (2 minutes)
def check(reaction, user):
return True if user != bot.user and str(reaction.emoji) in ['⏪', '⏩'] and reaction.message.id == msg.id else False
try:
reaction, user = await bot.wait_for('reaction_add', timeout=120, check=check)
except asyncio.TimeoutError:
pass
else:
# redirect on reaction
if reaction is None:
return
elif reaction.emoji == '⏪' and start > 0:
await embed_list_paginated(ctx, bot, pre, items, item_fct, base_embed, footer_prefix=footer_prefix, msg=msg, start=start-per_page, per_page=per_page)
elif reaction.emoji == '⏩' and items.__len__() > start+per_page:
await embed_list_paginated(ctx, bot, pre, items, item_fct, base_embed, footer_prefix=footer_prefix, msg=msg, start=start+per_page, per_page=per_page)
+14
View File
@@ -0,0 +1,14 @@
import random
adjs = ['able', 'aching', 'acidic', 'active', 'actual', 'adept', 'admired', 'adored', 'afraid', 'aged', 'agile', 'ajar', 'alarmed', 'alert', 'alive', 'all', 'amazing', 'ample', 'amused', 'amusing', 'ancient', 'angelic', 'angry', 'annual', 'another', 'antique', 'anxious', 'any', 'apt', 'arctic', 'arid', 'ashamed', 'assured', 'austere', 'average', 'aware', 'awesome', 'awful', 'awkward', 'babyish', 'back', 'bad', 'baggy', 'bare', 'barren', 'basic', 'belated', 'beloved', 'best', 'better', 'big', 'bitter', 'black', 'bland', 'blank', 'blaring', 'bleak', 'blind', 'blond', 'blue', 'bogus', 'boiling', 'bold', 'bony', 'boring', 'bossy', 'both', 'bouncy', 'bowed', 'brave', 'brief', 'bright', 'brisk', 'broken', 'bronze', 'brown', 'bruised', 'bubbly', 'bulky', 'bumpy', 'buoyant', 'burly', 'busy', 'buttery', 'buzzing', 'calm', 'candid', 'canine', 'capital', 'careful', 'caring', 'cheap', 'cheery', 'chief', 'chilly', 'chubby', 'classic', 'clean', 'clear', 'clever', 'close', 'closed', 'cloudy', 'clumsy', 'coarse', 'cold', 'common', 'complex', 'content', 'cooked', 'cool', 'corny', 'corrupt', 'costly', 'crafty', 'crazy', 'creamy', 'creepy', 'crisp', 'crooked', 'crowded', 'cruel', 'cuddly', 'curly', 'curvy', 'cute', 'damaged', 'damp', 'dapper', 'daring', 'dark', 'darling', 'dead', 'deadly', 'dear', 'dearest', 'decent', 'decimal', 'deep', 'defiant', 'delayed', 'dense', 'dental', 'devoted', 'digital', 'dim', 'dimpled', 'direct', 'dirty', 'dismal', 'distant', 'dizzy', 'dopey', 'doting', 'double', 'drab', 'drafty', 'dreary', 'droopy', 'dry', 'dual', 'dull', 'dutiful', 'each', 'eager', 'early', 'earnest', 'easy', 'edible', 'elastic', 'elated', 'elderly', 'elegant', 'eminent', 'empty', 'enraged', 'entire', 'envious', 'equal', 'ethical', 'even', 'every', 'evil', 'exalted', 'excited', 'exotic', 'expert', 'failing', 'faint', 'fair', 'fake', 'false', 'famous', 'fancy', 'far', 'far-off', 'faraway', 'fast', 'fat', 'fatal', 'fearful', 'feisty', 'feline', 'female', 'few', 'fickle', 'filthy', 'fine', 'firm', 'first', 'fitting', 'fixed', 'flaky', 'flashy', 'flat', 'flawed', 'flimsy', 'flowery', 'fluffy', 'fluid', 'focused', 'fond', 'foolish', 'forked', 'formal', 'frail', 'frank', 'frayed', 'free', 'French', 'fresh', 'frigid', 'frilly', 'frizzy', 'front', 'frosty', 'frozen', 'frugal', 'full', 'funny', 'fussy', 'fuzzy', 'gaseous', 'general', 'gentle', 'genuine', 'giant', 'giddy', 'gifted', 'giving', 'glaring', 'glass', 'gleeful', 'gloomy', 'glossy', 'glum', 'golden', 'good', 'grand', 'grave', 'gray', 'great', 'greedy', 'green', 'grim', 'grimy', 'gross', 'grouchy', 'growing', 'grown', 'grubby', 'grumpy', 'guilty', 'gummy', 'hairy', 'half', 'handy', 'happy', 'hard', 'harmful', 'harsh', 'hasty', 'hateful', 'healthy', 'hearty', 'heavy', 'hefty', 'helpful', 'hidden', 'hideous', 'high', 'hoarse', 'hollow', 'homely', 'honest', 'honored', 'hopeful', 'hot', 'huge', 'humble', 'humming', 'hungry', 'hurtful', 'husky', 'icky', 'icy', 'ideal', 'idiotic', 'idle', 'ill', 'illegal', 'immense', 'impish', 'impure', 'inborn', 'intent', 'itchy', 'jaded', 'jagged', 'jaunty', 'jealous', 'jittery', 'joint', 'jolly', 'jovial', 'joyful', 'joyous', 'juicy', 'jumbo', 'jumpy', 'junior', 'keen', 'key', 'kind', 'kindly', 'klutzy', 'knobby', 'knotty', 'knowing', 'known', 'kooky', 'kosher', 'lame', 'lanky', 'large', 'last', 'lasting', 'late', 'lavish', 'lawful', 'lazy', 'leading', 'leafy', 'lean', 'left', 'legal', 'light', 'likable', 'likely', 'limited', 'limp', 'limping', 'linear', 'lined', 'liquid', 'little', 'live', 'lively', 'livid', 'lone', 'lonely', 'long', 'loose', 'lost', 'loud', 'lovable', 'lovely', 'loving', 'low', 'loyal', 'lucky', 'lumpy', 'mad', 'made-up', 'major', 'male', 'mammoth', 'married', 'massive', 'mature', 'meager', 'mealy', 'mean', 'measly', 'meaty', 'medical', 'medium', 'meek', 'mellow', 'melodic', 'merry', 'messy', 'mild', 'milky', 'minor', 'minty', 'miserly', 'misty', 'mixed', 'modern', 'modest', 'moist', 'monthly', 'moral', 'muddy', 'muffled', 'mundane', 'murky', 'mushy', 'musty', 'muted', 'naive', 'narrow', 'nasty', 'natural', 'naughty', 'near', 'neat', 'needy', 'nervous', 'new', 'next', 'nice', 'nifty', 'nimble', 'nippy', 'noisy', 'nonstop', 'normal', 'notable', 'noted', 'novel', 'noxious', 'numb', 'nutty', 'obese', 'oblong', 'obvious', 'odd', 'oddball', 'offbeat', 'oily', 'old', 'only', 'open', 'optimal', 'opulent', 'orange', 'orderly', 'organic', 'ornate', 'ornery', 'other', 'our', 'oval', 'overdue', 'pale', 'paltry', 'parched', 'partial', 'past', 'pastel', 'peppery', 'perfect', 'perky', 'pesky', 'petty', 'phony', 'pink', 'pitiful', 'plain', 'plastic', 'playful', 'pleased', 'plump', 'plush', 'pointed', 'poised', 'polite', 'poor', 'popular', 'portly', 'posh', 'potable', 'present', 'pretty', 'pricey', 'prickly', 'primary', 'prime', 'private', 'prize', 'profuse', 'proper', 'proud', 'prudent', 'pungent', 'puny', 'pure', 'purple', 'pushy', 'putrid', 'puzzled', 'quaint', 'queasy', 'quick', 'quiet', 'quirky', 'radiant', 'ragged', 'rapid', 'rare', 'rashLine truncated
animals = ['frog', 'newt', 'tadpole', 'toad', 'spider', 'biddy', 'canary', 'crow', 'cuckoo', 'dove,', 'pigeon', 'duck', 'eagle', 'falcon', 'finch', 'goose', 'gull', 'hawk', 'jackdaw', 'jay', 'kestrel', 'mallard', 'ostrich', 'owl', 'parrot', 'peacock', 'pelican', 'penguin', 'piranha', 'raven', 'robin', 'rooster', 'sparrow', 'stork', 'swallow', 'swan', 'swift', 'tit', 'turkey', 'vulture', 'wren', 'barbel', 'carp', 'cod', 'crab', 'eel', 'haddock', 'halibut', 'lobster', 'perch', 'pike', 'plaice', 'ray', 'salmon', 'sawfish', 'scallop', 'shark', 'shell', 'shrimp', 'trout', 'ant', 'aphid', 'bee', 'beetle', 'flea', 'fly', 'gadfly', 'ladybug', 'larva', 'maggot', 'midge', 'moth', 'nymph', 'wasp', 'badger', 'bat', 'bear', 'beaver', 'bullock', 'camel', 'dolphin', 'fox', 'gazelle', 'gerbil', 'giraffe', 'goat', 'hamster', 'hare', 'hare', 'horse', 'hyena', 'lion', 'llama', 'lynx', 'mammoth', 'marmot', 'mink', 'mole', 'mouse', 'mule', 'otter', 'panda', 'polecat', 'pony', 'puma', 'racoon', 'rat', 'seal', 'seal', 'sheep', 'skunk', 'sloth', 'tiger', 'weasel', 'whale', 'wolf', 'zebra', 'slug', 'snail', 'boa', 'gecko', 'iguana', 'lizard', 'python', 'saurian', 'snake', 'leech']
async def generate_word(bot, server_id):
exists = 1
while exists is not None:
ad = random.sample(adjs, 1)
an = random.sample(animals, 1)
short = ad[0].capitalize() + an[0].capitalize()
exists = await bot.db.polls.find_one({'server_id': server_id, 'short': short})
return short