added docker configs
This commit is contained in:
1 parent
a89f0df161
commit
5cb6bccd7f
28 files changed
+97
-13
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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
@@ -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))
|
||||
@@ -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))
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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 = {}
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+1511
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
[](https://discordbots.org/bot/444514223075360800)
|
||||
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user