1
0
Fork 0
mirror of synced 2024-05-15 18:12:31 +12:00

Rewrite of database/configuration

This commit is contained in:
phxntxm 2019-01-27 20:58:39 -06:00
parent 68ccd02fba
commit 75af7dcd6f
26 changed files with 1604 additions and 2195 deletions

110
bot.py
View file

@ -1,4 +1,3 @@
#!/usr/local/bin/python3.5
import discord import discord
import traceback import traceback
import logging import logging
@ -24,35 +23,23 @@ bot = commands.AutoShardedBot(**opts)
logging.basicConfig(level=logging.INFO, filename='bonfire.log') logging.basicConfig(level=logging.INFO, filename='bonfire.log')
@bot.before_invoke
async def start_typing(ctx):
await ctx.trigger_typing()
@bot.event @bot.event
async def on_command_completion(ctx): async def on_command_completion(ctx):
author = ctx.message.author author = ctx.author.id
server = ctx.message.guild guild = ctx.guild.id if ctx.guild else None
command = ctx.command command = ctx.command.qualified_name
command_usage = await bot.db.actual_load( await bot.db.execute(
'command_usage', key=command.qualified_name "INSERT INTO command_usage(command, guild, author) VALUES ($1, $2, $3)",
) or {'command': command.qualified_name} command,
guild,
# Add one to the total usage for this command, basing it off 0 to start with (obviously) author
total_usage = command_usage.get('total_usage', 0) + 1 )
command_usage['total_usage'] = total_usage
# Add one to the author's usage for this command
total_member_usage = command_usage.get('member_usage', {})
member_usage = total_member_usage.get(str(author.id), 0) + 1
total_member_usage[str(author.id)] = member_usage
command_usage['member_usage'] = total_member_usage
# Add one to the server's usage for this command
if ctx.message.guild is not None:
total_server_usage = command_usage.get('server_usage', {})
server_usage = total_server_usage.get(str(server.id), 0) + 1
total_server_usage[str(server.id)] = server_usage
command_usage['server_usage'] = total_server_usage
# Save all the changes
await bot.db.save('command_usage', command_usage)
# Now add credits to a users amount # Now add credits to a users amount
# user_credits = bot.db.load('credits', key=ctx.author.id, pluck='credits') or 1000 # user_credits = bot.db.load('credits', key=ctx.author.id, pluck='credits') or 1000
@ -66,43 +53,35 @@ async def on_command_completion(ctx):
@bot.event @bot.event
async def on_command_error(ctx, error): async def on_command_error(ctx, error):
if isinstance(error, commands.CommandNotFound): error = error.original if hasattr(error, "original") else error
return ignored_errors = (
if isinstance(error, commands.DisabledCommand): commands.CommandNotFound,
return commands.DisabledCommand,
try: discord.Forbidden,
if isinstance(error.original, discord.Forbidden): aiohttp.ClientOSError,
return commands.CheckFailure,
elif isinstance(error.original, discord.HTTPException) and ( commands.CommandOnCooldown,
'empty message' in str(error.original) or )
'INTERNAL SERVER ERROR' in str(error.original) or
'REQUEST ENTITY TOO LARGE' in str(error.original) or
'Unknown Message' in str(error.original) or
'Origin Time-out' in str(error.original) or
'Bad Gateway' in str(error.original) or
'Gateway Time-out' in str(error.original) or
'Explicit content' in str(error.original)):
return
elif isinstance(error.original, aiohttp.ClientOSError):
return
elif isinstance(error.original, discord.NotFound) and 'Unknown Channel' in str(error.original):
return
except AttributeError: if isinstance(error, ignored_errors):
pass return
elif isinstance(error, discord.HTTPException) and (
'empty message' in str(error) or
'INTERNAL SERVER ERROR' in str(error) or
'REQUEST ENTITY TOO LARGE' in str(error) or
'Unknown Message' in str(error) or
'Origin Time-out' in str(error) or
'Bad Gateway' in str(error) or
'Gateway Time-out' in str(error) or
'Explicit content' in str(error)):
return
elif isinstance(error, discord.NotFound) and 'Unknown Channel' in str(error):
return
try: try:
if isinstance(error, commands.BadArgument): if isinstance(error, commands.BadArgument):
fmt = "Please provide a valid argument to pass to the command: {}".format(error) fmt = "Please provide a valid argument to pass to the command: {}".format(error)
await ctx.message.channel.send(fmt) await ctx.message.channel.send(fmt)
elif isinstance(error, commands.CheckFailure):
fmt = "You can't tell me what to do!"
# await ctx.message.channel.send(fmt)
elif isinstance(error, commands.CommandOnCooldown):
m, s = divmod(error.retry_after, 60)
fmt = "This command is on cooldown! Hold your horses! >:c\nTry again in {} minutes and {} seconds" \
.format(round(m), round(s))
# await ctx.message.channel.send(fmt)
elif isinstance(error, commands.NoPrivateMessage): elif isinstance(error, commands.NoPrivateMessage):
fmt = "This command cannot be used in a private message" fmt = "This command cannot be used in a private message"
await ctx.message.channel.send(fmt) await ctx.message.channel.send(fmt)
@ -113,21 +92,20 @@ async def on_command_error(ctx, error):
with open("error_log", 'a') as f: with open("error_log", 'a') as f:
print("In server '{0.message.guild}' at {1}\nFull command: `{0.message.content}`".format(ctx, str(now)), print("In server '{0.message.guild}' at {1}\nFull command: `{0.message.content}`".format(ctx, str(now)),
file=f) file=f)
try: traceback.print_tb(error.__traceback__, file=f)
traceback.print_tb(error.original.__traceback__, file=f) print('{0.__class__.__name__}: {0}'.format(error), file=f)
print('{0.__class__.__name__}: {0}'.format(error.original), file=f)
except Exception:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
except discord.HTTPException: except discord.HTTPException:
pass pass
if __name__ == '__main__': if __name__ == '__main__':
bot.loop.create_task(utils.db_check())
bot.remove_command('help') bot.remove_command('help')
# Setup our bot vars, db and cache
bot.db = utils.DB() bot.db = utils.DB()
bot.cache = utils.Cache(bot.db)
# Start our startup tasks
bot.loop.create_task(bot.db.setup())
bot.loop.create_task(bot.cache.setup())
for e in utils.extensions: for e in utils.extensions:
bot.load_extension(e) bot.load_extension(e)

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,17 @@
import discord import discord
import pendulum import datetime
import asyncio import asyncio
import traceback import traceback
import re import re
import calendar
from discord.ext import commands from discord.ext import commands
from asyncpg import UniqueViolationError
import utils import utils
tzmap = {
'us-central': pendulum.timezone('US/Central'),
'eu-central': pendulum.timezone('Europe/Paris'),
'hongkong': pendulum.timezone('Hongkong'),
}
def sort_birthdays(bds):
# First sort the birthdays based on the comparison of the actual date
bds = sorted(bds, key=lambda x: x['birthday'])
# We want to split this into birthdays after and before todays date
# We can then use this to sort based on "whose is closest"
later_bds = []
previous_bds = []
# Loop through each birthday
for bd in bds:
# If it is after or equal to today, insert into our later list
if bd['birthday'] >= pendulum.today().date():
later_bds.append(bd)
# Otherwise, insert into our previous list
else:
previous_bds.append(bd)
# At this point we have 2 lists, in order, one from all of dates before today, and one after
# So all we need to do is put them in order all of "laters" then all of "befores"
return later_bds + previous_bds
def parse_string(date): def parse_string(date):
year = pendulum.now().year today = datetime.date.today()
month = None month = None
day = None day = None
month_map = { month_map = {
@ -74,84 +49,104 @@ def parse_string(date):
elif part in month_map: elif part in month_map:
month = month_map.get(part) month = month_map.get(part)
if month and day: if month and day:
return pendulum.date(year, month, day) year = today.year
if month < today.month:
year += 1
elif month == today.month and day <= today.day:
year += 1
return datetime.date(year, month, day)
class Birthday: class Birthday:
"""Track and announcebirthdays""" """Track and announce birthdays"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.task = self.bot.loop.create_task(self.birthday_task()) self.task = self.bot.loop.create_task(self.birthday_task())
def get_birthdays_for_server(self, server, today=False): async def get_birthdays_for_server(self, server, today=False):
bds = self.bot.db.load('birthdays') members = ", ".join(f"{m.id}" for m in server.members)
# Get a list of the ID's to compare against query = f"""
member_ids = [str(m.id) for m in server.members] SELECT
id, birthday
FROM
users
WHERE
id IN ({members})
"""
if today:
query += """
AND
birthday = CURRENT_DATE
"""
query += """
ORDER BY
birthday
"""
# Now create a list comparing to the server's list of member IDs return await self.bot.db.fetch(query)
bds = [
bd
for member_id, bd in bds.items()
if str(member_id) in member_ids
]
_entries = []
for bd in bds:
if not bd['birthday']:
continue
day = parse_string(bd['birthday'])
# tz = tzmap.get(server.region)
# Check if it's today, and we want to only get todays birthdays
if (today and day == pendulum.today().date()) or not today:
# If so, get the member and add them to the entry
member = server.get_member(int(bd['member_id']))
_entries.append({
'birthday': day,
'member': member
})
return sort_birthdays(_entries)
async def birthday_task(self): async def birthday_task(self):
while True: await self.bot.wait_until_ready()
while not self.bot.is_closed():
try: try:
await self.notify_birthdays() await self.notify_birthdays()
except Exception as error: except Exception as error:
with open("error_log", 'a') as f: with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f) traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f) print(f"{error.__class__.__name__}: {error}", file=f)
finally: finally:
# Every 12 hours, this is not something that needs to happen often # Every day
await asyncio.sleep(60 * 60 * 12) await asyncio.sleep(60 * 60 * 24)
async def notify_birthdays(self): async def notify_birthdays(self):
tfilter = {'birthdays_allowed': True} query = """
servers = await self.bot.db.actual_load('server_settings', table_filter=tfilter) SELECT
id, COALESCE(birthday_alerts, default_alerts) AS channel
FROM
guilds
WHERE
birthday_notifications=True
AND
COALESCE(birthday_alerts, default_alerts) IS NOT NULL
"""
servers = await self.bot.db.fetch(query)
update_bds = []
if not servers:
return
for s in servers: for s in servers:
server = self.bot.get_guild(int(s['server_id'])) # Get the channel based on the birthday alerts, or default alerts channel
if not server: channel = self.bot.get_channel(s['channel'])
if not channel:
continue continue
# Set our default to either the one set bds = await self.get_birthdays_for_server(channel.guild, today=True)
default_channel_id = s.get('notifications', {}).get('default')
# If it is has been overriden by picarto notifications setting, use this
channel_id = s.get('notifications', {}).get('birthday') or default_channel_id
if not channel_id:
continue
# Now get the channel based on that ID # A list of the id's that will get updated
channel = server.get_channel(int(channel_id))
bds = self.get_birthdays_for_server(server, today=True)
for bd in bds: for bd in bds:
try: try:
await channel.send("It is {}'s birthday today! " await channel.send(f"It is {bd['member'].mention}'s birthday today! "
"Wish them a happy birthday! \N{SHORTCAKE}".format(bd['member'].mention)) "Wish them a happy birthday! \N{SHORTCAKE}")
except (discord.Forbidden, discord.HTTPException, AttributeError): except (discord.Forbidden, discord.HTTPException):
pass pass
finally:
update_bds.append(bd['id'])
if not update_bds:
return
query = f"""
UPDATE
users
SET
birthday = birthday + interval '1 year'
WHERE
id IN ({", ".join(f"'{bd}'" for bd in update_bds)})
"""
print(query)
await self.bot.db.execute(query)
@commands.group(aliases=['birthdays'], invoke_without_command=True) @commands.group(aliases=['birthdays'], invoke_without_command=True)
@commands.guild_only() @commands.guild_only()
@ -162,20 +157,29 @@ class Birthday:
EXAMPLE: !birthdays EXAMPLE: !birthdays
RESULT: A printout of the birthdays from everyone on this server""" RESULT: A printout of the birthdays from everyone on this server"""
if member: if member:
date = self.bot.db.load('birthdays', key=member.id, pluck='birthday') date = await self.bot.db.fetchrow("SELECT birthday FROM users WHERE id=$1", member.id)
date = date['birthday']
if date: if date:
await ctx.send("{}'s birthday is {}".format(member.display_name, date)) await ctx.send(f"{member.display_name}'s birthday is {calendar.month_name[date.month]} {date.day}")
else: else:
await ctx.send("I do not have {}'s birthday saved!".format(member.display_name)) await ctx.send(f"I do not have {member.display_name}'s birthday saved!")
else: else:
# Get this server's birthdays # Get this server's birthdays
bds = self.get_birthdays_for_server(ctx.message.guild) bds = await self.get_birthdays_for_server(ctx.guild)
# Create entries based on the user's display name and their birthday # Create entries based on the user's display name and their birthday
entries = ["{} ({})".format(bd['member'].display_name, bd['birthday'].strftime("%B %-d")) for bd in bds] entries = [
f"{ctx.guild.get_member(bd['id']).display_name} ({bd['birthday'].strftime('%B %-d')})"
for bd in bds
if bd['birthday']
]
if not entries:
await ctx.send("I don't know anyone's birthday in this server!")
return
# Create our pages object # Create our pages object
try: try:
pages = utils.Pages(ctx, entries=entries, per_page=5) pages = utils.Pages(ctx, entries=entries, per_page=5)
pages.title = "Birthdays for {}".format(ctx.message.guild.name) pages.title = f"Birthdays for {ctx.guild.name}"
await pages.paginate() await pages.paginate()
except utils.CannotPaginate as e: except utils.CannotPaginate as e:
await ctx.send(str(e)) await ctx.send(str(e))
@ -196,13 +200,11 @@ class Birthday:
await ctx.send("Please provide date in a valid format, such as December 1st!") await ctx.send("Please provide date in a valid format, such as December 1st!")
return return
date = date.strftime("%B %-d") await ctx.send(f"I have just saved your birthday as {date}")
entry = { try:
'member_id': str(ctx.message.author.id), await self.bot.db.execute("INSERT INTO users (id, birthday) VALUES ($1, $2)", ctx.author.id, date)
'birthday': date except UniqueViolationError:
} await self.bot.db.execute("UPDATE users SET birthday = $1 WHERE id = $2", date, ctx.author.id)
await self.bot.db.save('birthdays', entry)
await ctx.send("I have just saved your birthday as {}".format(date))
@birthday.command(name='remove') @birthday.command(name='remove')
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
@ -211,30 +213,8 @@ class Birthday:
EXAMPLE: !birthday remove EXAMPLE: !birthday remove
RESULT: I have magically forgotten your birthday""" RESULT: I have magically forgotten your birthday"""
entry = {
'member_id': str(ctx.message.author.id),
'birthday': None
}
await self.bot.db.save('birthdays', entry)
await ctx.send("I don't know your birthday anymore :(") await ctx.send("I don't know your birthday anymore :(")
await self.bot.db.execute("UPDATE users SET birthday=NULL WHERE id=$1", ctx.author.id)
@birthday.command(name='alerts', aliases=['notifications'])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def birthday_alerts_channel(self, ctx, channel: discord.TextChannel):
"""Sets the notifications channel for birthday notifications
EXAMPLE: !birthday alerts #birthday
RESULT: birthday notifications will go to this channel
"""
entry = {
'server_id': str(ctx.message.guild.id),
'notifications': {
'birthday': str(channel.id)
}
}
await self.bot.db.save('server_settings', entry)
await ctx.send("All birthday notifications will now go to {}".format(channel.mention))
def setup(bot): def setup(bot):

556
cogs/config.py Normal file
View file

@ -0,0 +1,556 @@
from discord.ext import commands
from asyncpg import UniqueViolationError
import utils
import discord
valid_perms = [p for p in dir(discord.Permissions) if isinstance(getattr(discord.Permissions, p), property)]
class ConfigException(Exception):
pass
class WrongSettingType(ConfigException):
def __init__(self, message):
self.message = message
class MessageFormatError(ConfigException):
def __init__(self, original, keys):
self.original = original
self.keys = keys
class GuildConfiguration:
"""Handles configuring the different settings that can be used on the bot"""
def _str_to_bool(self, opt, setting):
setting = setting.title()
if setting.title() not in ["True", "False"]:
raise WrongSettingType(
f"The {opt} setting requires either 'True' or 'False', not {setting}"
)
return setting.title() == "True"
async def _get_channel(self, ctx, setting):
converter = commands.converter.TextChannelConverter()
return await converter.convert(ctx, setting)
async def _set_db_guild_opt(self, opt, setting, ctx):
try:
return await ctx.bot.db.execute(f"INSERT INTO guilds (id, {opt}) VALUES ($1, $2)", ctx.guild.id, setting)
except UniqueViolationError:
return await ctx.bot.db.execute(f"UPDATE guilds SET {opt} = $1 WHERE id = $2", setting, ctx.guild.id)
# These are handles for each setting type
async def _handle_set_birthday_notifications(self, ctx, setting):
opt = "birthday_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_welcome_notifications(self, ctx, setting):
opt = "welcome_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_goodbye_notifications(self, ctx, setting):
opt = "goodbye_notifications"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_colour_roles(self, ctx, setting):
opt = "colour_roles"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_include_default_battles(self, ctx, setting):
opt = "include_default_battles"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_include_default_hugs(self, ctx, setting):
opt = "include_default_hugs"
setting = self._str_to_bool(opt, setting)
return await self._set_db_guild_opt(opt, setting, ctx)
async def _handle_set_welcome_msg(self, ctx, setting):
try:
setting.format(member='test', server='test')
except KeyError as e:
raise MessageFormatError(e, ["member", "server"])
else:
return await self._set_db_guild_opt("welcome_msg", setting, ctx)
async def _handle_set_goodbye_msg(self, ctx, setting):
try:
setting.format(member='test', server='test')
except KeyError as e:
raise MessageFormatError(e, ["member", "server"])
else:
return await self._set_db_guild_opt("goodbye_msg", setting, ctx)
async def _handle_set_prefix(self, ctx, setting):
if len(setting) > 20:
raise WrongSettingType("Please keep the prefix under 20 characters")
if setting.lower().strip() == "none":
setting = None
result = await self._set_db_guild_opt("prefix", setting, ctx)
# We want to update our cache for prefixes
ctx.bot.cache.update_prefix(ctx.guild, setting)
return result
async def _handle_set_default_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("default_alerts", channel.id, ctx)
async def _handle_set_welcome_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("welcome_alerts", channel.id, ctx)
async def _handle_set_goodbye_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("goodbye_alerts", channel.id, ctx)
async def _handle_set_picarto_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("picarto_alerts", channel.id, ctx)
async def _handle_set_birthday_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("birthday_alerts", channel.id, ctx)
async def _handle_set_raffle_alerts(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
return await self._set_db_guild_opt("raffle_alerts", channel.id, ctx)
async def _handle_set_followed_picarto_channels(self, ctx, setting):
user = await utils.request(f"http://api.picarto.tv/v1/channel/name/{setting}")
if user is None:
raise WrongSettingType(f"Could not find a picarto user with the username {setting}")
query = """
UPDATE
guilds
SET
followed_picarto_channels = array_append(followed_picarto_channels, $1)
WHERE
id=$2 AND
NOT $1 = ANY(followed_picarto_channels);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_ignored_channels(self, ctx, setting):
channel = await self._get_channel(ctx, setting)
query = """
UPDATE
guilds
SET
ignored_channels = array_append(ignored_channels, $1)
WHERE
id=$2 AND
NOT $1 = ANY(ignored_channels);
"""
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
async def _handle_set_ignored_members(self, ctx, setting):
# We want to make it possible to have members that aren't in the server ignored
# So first check if it's a digit (the id)
if not setting.isdigit():
converter = commands.converter.MemberConverter()
member = await converter.convert(ctx, setting)
setting = member.id
query = """
UPDATE
guilds
SET
ignored_members = array_append(ignored_members, $1)
WHERE
id=$2 AND
NOT $1 = ANY(ignored_members);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_rules(self, ctx, setting):
query = """
UPDATE
guilds
SET
rules = array_append(rules, $1)
WHERE
id=$2 AND
NOT $1 = ANY(rules);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_assignable_roles(self, ctx, setting):
converter = commands.converter.RoleConverter()
role = await converter.convert(ctx, setting)
query = """
UPDATE
guilds
SET
assignable_roles = array_append(assignable_roles, $1)
WHERE
id=$2 AND
NOT $1 = ANY(assignable_roles);
"""
return await ctx.bot.db.execute(query, role.id, ctx.guild.id)
async def _handle_set_custom_battles(self, ctx, setting):
try:
setting.format(loser="player1", winner="player2")
except KeyError as e:
raise MessageFormatError(e, ["loser", "winner"])
else:
query = """
UPDATE
guilds
SET
custom_battles = array_append(custom_battles, $1)
WHERE
id=$2 AND
NOT $1 = ANY(custom_battles);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_set_custom_hugs(self, ctx, setting):
try:
setting.format(user="user")
except KeyError as e:
raise MessageFormatError(e, ["user"])
else:
query = """
UPDATE
guilds
SET
custom_hugs = array_append(custom_hugs, $1)
WHERE
id=$2 AND
NOT $1 = ANY(custom_hugs);
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_birthday_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("birthday_notifications", False, ctx)
async def _handle_remove_welcome_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_notifications", False, ctx)
async def _handle_remove_goodbye_notifications(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_notifications", False, ctx)
async def _handle_remove_colour_roles(self, ctx, setting=None):
return await self._set_db_guild_opt("colour_roles", False, ctx)
async def _handle_remove_include_default_battles(self, ctx, setting=None):
return await self._set_db_guild_opt("include_default_battles", False, ctx)
async def _handle_remove_include_default_hugs(self, ctx, setting=None):
return await self._set_db_guild_opt("include_default_hugs", False, ctx)
async def _handle_remove_welcome_msg(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_msg", None, ctx)
async def _handle_remove_goodbye_msg(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_msg", None, ctx)
async def _handle_remove_prefix(self, ctx, setting=None):
return await self._set_db_guild_opt("prefix", None, ctx)
async def _handle_remove_default_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("default_alerts", None, ctx)
async def _handle_remove_welcome_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("welcome_alerts", None, ctx)
async def _handle_remove_goodbye_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("goodbye_alerts", None, ctx)
async def _handle_remove_picarto_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("picarto_alerts", None, ctx)
async def _handle_remove_birthday_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("birthday_alerts", None, ctx)
async def _handle_remove_raffle_alerts(self, ctx, setting=None):
return await self._set_db_guild_opt("raffle_alerts", None, ctx)
async def _handle_remove_followed_picarto_channels(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
query = """
UPDATE
guilds
SET
followed_picarto_channels = array_remove(followed_picarto_channels, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_ignored_channels(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
channel = await self._get_channel(ctx, setting)
query = """
UPDATE
guilds
SET
ignored_channels = array_remove(ignored_channels, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, channel.id, ctx.guild.id)
async def _handle_remove_ignored_members(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
# We want to make it possible to have members that aren't in the server ignored
# So first check if it's a digit (the id)
if not setting.isdigit():
converter = commands.converter.MemberConverter()
member = await converter.convert(ctx, setting)
setting = member.id
query = """
UPDATE
guilds
SET
ignored_members = array_remove(ignored_members, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_rules(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the rule you want to remove")
query = """
UPDATE
guilds
SET
rules = array_remove(rules, rules[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_assignable_roles(self, ctx, setting=None):
if setting is None:
raise WrongSettingType("Specifying which channel you want to remove is required")
if not setting.isdigit():
converter = commands.converter.RoleConverter()
role = await converter.convert(ctx, setting)
setting = role.id
query = """
UPDATE
guilds
SET
assignable_roles = array_remove(assignable_roles, $1)
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_custom_battles(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the custom battle you want to remove")
query = """
UPDATE
guilds
SET
custom_battles = array_remove(custom_battles, rules[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def _handle_remove_custom_hugs(self, ctx, setting=None):
if setting is None or not setting.isdigit():
raise WrongSettingType("Please provide the number of the custom hug you want to remove")
query = """
UPDATE
guilds
SET
custom_hugs = array_remove(custom_hugs, rules[$1])
WHERE
id=$2
"""
return await ctx.bot.db.execute(query, setting, ctx.guild.id)
async def __after_invoke(self, ctx):
"""Here we will facilitate cleaning up settings, will remove channels/roles that no longer exist, etc."""
pass
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def config(self, ctx, *, opt=None):
"""Handles the configuration of the bot for this server"""
if opt:
setting = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
if setting and opt in setting:
setting = await utils.convert(ctx, str(setting[opt])) or setting[opt]
await ctx.send(f"{opt} is set to:\n{setting}")
return
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
# For convenience, if it's None, just create it and return the default values
if settings is None:
await ctx.bot.db.execute("INSERT INTO guilds (id) VALUES ($1)", ctx.guild.id)
settings = await ctx.bot.db.fetchrow("SELECT * FROM guilds WHERE id=$1", ctx.guild.id)
alerts = {}
# This is dirty I know, but oh well...
for alert_type in ["default", "welcome", "goodbye", "picarto", "birthday", "raffle"]:
channel = ctx.guild.get_channel(settings.get(f"{alert_type}_alerts"))
name = channel.name if channel else None
alerts[alert_type] = name
fmt = f"""
**Notification Settings**
birthday_notifications
*Notify on the birthday that users in this guild have saved*
**{settings.get("birthday_notifications")}**
welcome_notifications
*Notify when someone has joined this guild*
**{settings.get("welcome_notifications")}**
goodbye_notifications
*Notify when someone has left this guild
**{settings.get("goodbye_notifications")}**
welcome_msg
*A message that can be customized and used when someone joins the server*
**{"Set" if settings.get("welcome_msg") is not None else "Not set"}**
goodbye_msg
*A message that can be customized and used when someone leaves the server*
**{"Set" if settings.get("goodbye_msg") is not None else "Not set"}**
**Alert Channels**
default_alerts
*The channel to default alert messages to*
**{alerts.get("default_alerts")}**
welcome_alerts
*The channel to send welcome alerts to (when someone joins the server)*
**{alerts.get("welcome_alerts")}**
goodbye_alerts
*The channel to send goodbye alerts to (when someone leaves the server)*
**{alerts.get("goodbye_alerts")}**
picarto_alerts
*The channel to send Picarto alerts to (when a channel the server follows goes on/offline)*
**{alerts.get("picarto_alerts")}**
birthday_alerts
*The channel to send birthday alerts to (on the day of someone's birthday)*
**{alerts.get("birthday_alerts")}**
raffle_alerts
*The channel to send alerts for server raffles to*
**{alerts.get("raffle_alerts")}**
**Misc Settings**
followed_picarto_channels
*Channels for the bot to "follow" and notify this server when they go live*
**{len(settings.get("followed_picarto_channels"))}**
ignored_channels
*Channels that the bot ignores*
**{len(settings.get("ignored_channels"))}**
ignored_members
*Members that the bot ignores*
**{len(settings.get("ignored_members"))}**
rules
*Rules for this server*
**{len(settings.get("rules"))}**
assignable_roles
*Roles that can be self-assigned by users*
**{len(settings.get("assignable_roles"))}**
custom_battles
*Possible outcomes to battles that can be received on this server*
**{len(settings.get("custom_battles"))}**
custom_hugs
*Possible outcomes to hugs that can be received on this server*
**{len(settings.get("custom_hugs"))}**
""".strip()
embed = discord.Embed(title=f"Configuration for {ctx.guild.name}", description=fmt)
embed.set_image(url=ctx.guild.icon_url)
await ctx.send(embed=embed)
@config.command(name="set", aliases=["add"])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def _set_setting(self, ctx, option, *, setting):
"""Sets one of the configuration settings for this server"""
try:
coro = getattr(self, f"_handle_set_{option}")
except AttributeError:
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
else:
try:
await coro(ctx, setting=setting)
except WrongSettingType as exc:
await ctx.send(exc.message)
except MessageFormatError as exc:
fmt = f"""
Failed to parse the format string provided, possible keys are: {', '.join(k for k in exc.keys)}
Extraneous args provided: {', '.join(k for k in exc.original.args)}
"""
await ctx.send(fmt)
except commands.BadArgument:
pass
else:
await ctx.send(f"{option} has succesfully been set to {setting}")
@config.command(name="unset", aliases=["remove"])
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def _remove_setting(self, ctx, option, *, setting=None):
"""Unsets/removes an option from one of the settings."""
try:
coro = getattr(self, f"_handle_remove_{option}")
except AttributeError:
await ctx.send(f"{option} is not a valid config option. Use {ctx.prefix}config to list all config options")
else:
try:
await coro(ctx, setting=setting)
except WrongSettingType as exc:
await ctx.send(exc.message)
except commands.BadArgument:
pass
else:
await ctx.send(f"{option} has succesfully been unset")
def setup(bot):
bot.add_cog(GuildConfiguration())

View file

@ -69,64 +69,46 @@ class StatsUpdate:
await self.update() await self.update()
async def on_member_join(self, member): async def on_member_join(self, member):
guild = member.guild query = """
server_settings = self.bot.db.load('server_settings', key=str(guild.id)) SELECT
COALESCE(welcome_alerts, default_alerts) AS channel,
welcome_msg AS msg
FROM
guilds
WHERE
welcome_notifications = True
AND
id = $1
AND
COALESCE(welcome_alerts, default_alerts) IS NOT NULL
"""
settings = await self.bot.db.fetchrow(query, member.guild.id)
message = settings['msg'] or "Welcome to the '{server}' server {member}!"
channel = member.guild.get_channel(settings['channel'])
try: try:
join_leave_on = server_settings['join_leave'] await channel.send(message.format(server=member.guild.name, member=member.mention))
if join_leave_on:
# Get the notifications settings, get the welcome setting
notifications = self.bot.db.load('server_settings', key=guild.id, pluck='notifications') or {}
# Set our default to either the one set, or the default channel of the server
default_channel_id = notifications.get('default')
# If it is has been overriden by picarto notifications setting, use this
channel_id = notifications.get('welcome') or default_channel_id
# Get the message if it exists
join_message = self.bot.db.load('server_settings', key=guild.id, pluck='welcome_message')
if not join_message:
join_message = "Welcome to the '{server}' server {member}!"
else:
return
except (IndexError, TypeError, KeyError):
return
if channel_id:
channel = guild.get_channel(int(channel_id))
else:
return
try:
await channel.send(join_message.format(server=guild.name, member=member.mention))
except (discord.Forbidden, discord.HTTPException, AttributeError): except (discord.Forbidden, discord.HTTPException, AttributeError):
pass pass
async def on_member_remove(self, member): async def on_member_remove(self, member):
guild = member.guild query = """
server_settings = self.bot.db.load('server_settings', key=str(guild.id)) SELECT
COALESCE(goodbye_alerts, default_alerts) AS channel,
goodbye_msg AS msg
FROM
guilds
WHERE
welcome_notifications = True
AND
id = $1
AND
COALESCE(goodbye_alerts, default_alerts) IS NOT NULL
"""
settings = await self.bot.db.fetchrow(query, member.guild.id)
message = settings['msg'] or "{member} has left the server, I hope it wasn't because of something I said :c"
channel = member.guild.get_channel(settings['channel'])
try: try:
join_leave_on = server_settings['join_leave'] await channel.send(message.format(server=member.guild.name, member=member.mention))
if join_leave_on:
# Get the notifications settings, get the welcome setting
notifications = self.bot.db.load('server_settings', key=guild.id, pluck='notifications') or {}
# Set our default to either the one set, or the default channel of the server
default_channel_id = notifications.get('default')
# If it is has been overriden by picarto notifications setting, use this
channel_id = notifications.get('welcome') or default_channel_id
# Get the message if it exists
leave_message = self.bot.db.load('server_settings', key=guild.id, pluck='goodbye_message')
if not leave_message:
leave_message = "{member} has left the server, I hope it wasn't because of something I said :c"
else:
return
except (IndexError, TypeError, KeyError):
return
if channel_id:
channel = guild.get_channel(int(channel_id))
else:
return
try:
await channel.send(leave_message.format(server=guild.name, member=member.name))
except (discord.Forbidden, discord.HTTPException, AttributeError): except (discord.Forbidden, discord.HTTPException, AttributeError):
pass pass

View file

@ -70,6 +70,7 @@ class Hangman:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.games = {} self.games = {}
self.pending_games = []
def create(self, word, ctx): def create(self, word, ctx):
# Create a new game, then save it as the server's game # Create a new game, then save it as the server's game
@ -101,7 +102,7 @@ class Hangman:
# We're creating a fmt variable, so that we can add a message for if a guess was correct or not # We're creating a fmt variable, so that we can add a message for if a guess was correct or not
# And also add a message for a loss/win # And also add a message for a loss/win
if len(guess) == 1: if len(guess) == 1:
if guess in game.guessed_letters: if guess.lower() in game.guessed_letters:
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send("That letter has already been guessed!") await ctx.send("That letter has already been guessed!")
# Return here as we don't want to count this as a failure # Return here as we don't want to count this as a failure
@ -142,6 +143,9 @@ class Hangman:
if self.games.get(ctx.message.guild.id) is not None: if self.games.get(ctx.message.guild.id) is not None:
await ctx.send("Sorry but only one Hangman game can be running per server!") await ctx.send("Sorry but only one Hangman game can be running per server!")
return return
if ctx.guild.id in self.pending_games:
await ctx.send("Someone has already started one, and I'm now waiting for them...")
return
try: try:
msg = await ctx.message.author.send( msg = await ctx.message.author.send(
@ -160,12 +164,16 @@ class Hangman:
def check(m): def check(m):
return m.channel == msg.channel and len(m.content) <= 30 return m.channel == msg.channel and len(m.content) <= 30
self.pending_games.append(ctx.guild.id)
try: try:
msg = await self.bot.wait_for('message', check=check, timeout=60) msg = await self.bot.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.pending_games.remove(ctx.guild.id)
await ctx.send( await ctx.send(
"You took too long! Please look at your DM's as that's where I'm asking for the phrase you want to use") "You took too long! Please look at your DM's as that's where I'm asking for the phrase you want to use")
return return
else:
self.pending_games.remove(ctx.guild.id)
forbidden_phrases = ['stop', 'delete', 'remove', 'end', 'create', 'start'] forbidden_phrases = ['stop', 'delete', 'remove', 'end', 'create', 'start']
if msg.content in forbidden_phrases: if msg.content in forbidden_phrases:

View file

@ -41,7 +41,7 @@ class Images:
result = await utils.request('https://random.dog/woof.json') result = await utils.request('https://random.dog/woof.json')
try: try:
url = result.get("url") url = result.get("url")
filename = re.match("https:\/\/random.dog\/(.*)", url).group(1) filename = re.match("https://random.dog/(.*)", url).group(1)
except AttributeError: except AttributeError:
await ctx.send("I couldn't connect! Sorry no dogs right now ;w;") await ctx.send("I couldn't connect! Sorry no dogs right now ;w;")
return return
@ -130,7 +130,6 @@ class Images:
EXAMPLE: !derpi Rainbow Dash EXAMPLE: !derpi Rainbow Dash
RESULT: A picture of Rainbow Dash!""" RESULT: A picture of Rainbow Dash!"""
await ctx.message.channel.trigger_typing()
if len(search) > 0: if len(search) > 0:
url = 'https://derpibooru.org/search.json' url = 'https://derpibooru.org/search.json'
@ -139,7 +138,7 @@ class Images:
query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value)) query = ' '.join(value for value in search if not re.search('&?filter_id=[0-9]+', value))
params = {'q': query} params = {'q': query}
nsfw = await utils.channel_is_nsfw(ctx.message.channel, self.bot.db) nsfw = utils.channel_is_nsfw(ctx.message.channel)
# If this is a nsfw channel, we just need to tack on 'explicit' to the terms # If this is a nsfw channel, we just need to tack on 'explicit' to the terms
# Also use the custom filter that I have setup, that blocks some certain tags # Also use the custom filter that I have setup, that blocks some certain tags
# If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit # If the channel is not nsfw, we don't need to do anything, as the default filter blocks explicit
@ -200,7 +199,6 @@ class Images:
EXAMPLE: !e621 dragon EXAMPLE: !e621 dragon
RESULT: A picture of a dragon (hopefully, screw your tagging system e621)""" RESULT: A picture of a dragon (hopefully, screw your tagging system e621)"""
await ctx.message.channel.trigger_typing()
# This changes the formatting for queries, so we don't # This changes the formatting for queries, so we don't
# Have to use e621's stupid formatting when using the command # Have to use e621's stupid formatting when using the command
@ -214,7 +212,7 @@ class Images:
'tags': tags 'tags': tags
} }
nsfw = await utils.channel_is_nsfw(ctx.message.channel, self.bot.db) nsfw = utils.channel_is_nsfw(ctx.message.channel)
# e621 by default does not filter explicit content, so tack on # e621 by default does not filter explicit content, so tack on
# safe/explicit based on if this channel is nsfw or not # safe/explicit based on if this channel is nsfw or not

View file

@ -1,13 +1,12 @@
import rethinkdb as r
from discord.ext import commands from discord.ext import commands
from discord.ext.commands.cooldowns import BucketType from discord.ext.commands.cooldowns import BucketType
from collections import defaultdict
import utils import utils
import discord import discord
import random import random
import functools import functools
import asyncio
battle_outcomes = \ battle_outcomes = \
["A meteor fell on {loser}, {winner} is left standing and has been declared the victor!", ["A meteor fell on {loser}, {winner} is left standing and has been declared the victor!",
@ -91,68 +90,36 @@ class Interaction:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.battles = {} self.battles = defaultdict(list)
self.bot.br = BattleRankings(self.bot)
self.bot.br.update_start()
def get_battle(self, player): def get_receivers_battle(self, receiver):
battles = self.battles.get(player.guild.id) for battle in self.battles.get(receiver.guild.id, []):
if battle.is_receiver(receiver):
if battles is None:
return None
for battle in battles:
if battle['p2'] == player.id:
return battle return battle
def can_battle(self, player): def can_initiate_battle(self, player):
battles = self.battles.get(player.guild.id) for battle in self.battles.get(player.guild.id, []):
if battle.is_initiator(player):
if battles is None:
return True
for x in battles:
if x['p1'] == player.id:
return False return False
return True return True
def can_be_battled(self, player): def can_receive_battle(self, player):
battles = self.battles.get(player.guild.id) for battle in self.battles.get(player.guild.id, []):
if battle.is_receiver(player):
if battles is None:
return True
for x in battles:
if x['p2'] == player.id:
return False return False
return True return True
def start_battle(self, player1, player2): def start_battle(self, initiator, receiver):
battles = self.battles.get(player1.guild.id, []) battle = Battle(initiator, receiver)
entry = { self.battles[initiator.guild.id].append(battle)
'p1': player1.id, return battle
'p2': player2.id
}
battles.append(entry)
self.battles[player1.guild.id] = battles
# Handles removing the author from the dictionary of battles # Handles removing the author from the dictionary of battles
def battling_off(self, player1=None, player2=None): def battling_off(self, battle):
if player1: for guild, battles in self.battles.items():
guild = player1.guild.id if battle in battles:
else: battles.remove(battle)
guild = player2.guild.id return
battles = self.battles.get(guild, [])
# Create a new list, exactly the way the last one was setup
# But don't include the one start with player's ID
new_battles = []
for b in battles:
if player1 and b['p1'] == player1.id:
continue
if player2 and b['p2'] == player2.id:
continue
new_battles.append(b)
self.battles[guild] = new_battles
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -166,7 +133,7 @@ class Interaction:
await ctx.send("Your arms aren't big enough") await ctx.send("Your arms aren't big enough")
return return
if user is None: if user is None:
user = ctx.message.author user = ctx.author
else: else:
converter = commands.converter.MemberConverter() converter = commands.converter.MemberConverter()
try: try:
@ -175,12 +142,12 @@ class Interaction:
await ctx.send("Error: Could not find user: {}".format(user)) await ctx.send("Error: Could not find user: {}".format(user))
return return
# Lets get the settings settings = await self.bot.db.fetchrow(
settings = self.bot.db.load('server_settings', key=ctx.message.guild.id) or {} "SELECT custom_hugs, include_default_hugs FROM guilds WHERE id = $1",
# Get the custom messages we can use ctx.guild.id
custom_msgs = settings.get('hugs') )
default_on = settings.get('default_hugs') custom_msgs = settings["custom_hugs"]
# if they exist, then we want to see if we want to use default as well default_on = settings["include_default_hugs"]
if custom_msgs: if custom_msgs:
if default_on or default_on is None: if default_on or default_on is None:
msgs = hugs + custom_msgs msgs = hugs + custom_msgs
@ -205,7 +172,7 @@ class Interaction:
# First check if everyone was mentioned # First check if everyone was mentioned
if ctx.message.mention_everyone: if ctx.message.mention_everyone:
await ctx.send("You want to battle {} people? Good luck with that...".format( await ctx.send("You want to battle {} people? Good luck with that...".format(
len(ctx.message.channel.members) - 1) len(ctx.channel.members) - 1)
) )
return return
# Then check if nothing was provided # Then check if nothing was provided
@ -221,7 +188,7 @@ class Interaction:
await ctx.send("Error: Could not find user: {}".format(player2)) await ctx.send("Error: Could not find user: {}".format(player2))
return return
# Then check if the person used is the author # Then check if the person used is the author
if ctx.message.author.id == player2.id: if ctx.author.id == player2.id:
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send("Why would you want to battle yourself? Suicide is not the answer") await ctx.send("Why would you want to battle yourself? Suicide is not the answer")
return return
@ -231,24 +198,24 @@ class Interaction:
await ctx.send("I always win, don't even try it.") await ctx.send("I always win, don't even try it.")
return return
# Next two checks are to see if the author or person battled can be battled # Next two checks are to see if the author or person battled can be battled
if not self.can_battle(ctx.message.author): if not self.can_initiate_battle(ctx.author):
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send("You are already battling someone!") await ctx.send("You are already battling someone!")
return return
if not self.can_be_battled(player2): if not self.can_receive_battle(player2):
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send("{} is already being challenged to a battle!".format(player2)) await ctx.send("{} is already being challenged to a battle!".format(player2))
return return
# Add the author and player provided in a new battle # Add the author and player provided in a new battle
self.start_battle(ctx.message.author, player2) battle = self.start_battle(ctx.author, player2)
fmt = "{0.message.author.mention} has challenged you to a battle {1.mention}\n" \ fmt = f"{ctx.author.mention} has challenged you to a battle {player2.mention}\n" \
"{0.prefix}accept or {0.prefix}decline" f"{ctx.prefix}accept or {ctx.prefix}decline"
# Add a call to turn off battling, if the battle is not accepted/declined in 3 minutes # Add a call to turn off battling, if the battle is not accepted/declined in 3 minutes
part = functools.partial(self.battling_off, player1=ctx.message.author) part = functools.partial(self.battling_off, battle)
self.bot.loop.call_later(180, part) self.bot.loop.call_later(180, part)
await ctx.send(fmt.format(ctx, player2)) await ctx.send(fmt)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -260,23 +227,23 @@ class Interaction:
RESULT: Hopefully the other person's death""" RESULT: Hopefully the other person's death"""
# This is a check to make sure that the author is the one being BATTLED # This is a check to make sure that the author is the one being BATTLED
# And not the one that started the battle # And not the one that started the battle
battle = self.get_battle(ctx.message.author) battle = self.get_receivers_battle(ctx.author)
if battle is None: if battle is None:
await ctx.send("You are not currently being challenged to a battle!") await ctx.send("You are not currently being challenged to a battle!")
return return
battleP1 = discord.utils.find(lambda m: m.id == battle['p1'], ctx.message.guild.members) if ctx.guild.get_member(battle.initiator.id) is None:
if battleP1 is None:
await ctx.send("The person who challenged you to a battle has apparently left the server....why?") await ctx.send("The person who challenged you to a battle has apparently left the server....why?")
self.battling_off(battle)
return return
battleP2 = ctx.message.author
# Lets get the settings # Lets get the settings
settings = self.bot.db.load('server_settings', key=ctx.message.guild.id) or {} settings = await self.bot.db.fetchrow(
# Get the custom messages we can use "SELECT custom_battles, include_default_battles FROM guilds WHERE id = $1",
custom_msgs = settings.get('battles') ctx.guild.id
default_on = settings.get('default_battles') )
custom_msgs = settings["custom_battles"]
default_on = settings["include_default_battles"]
# if they exist, then we want to see if we want to use default as well # if they exist, then we want to see if we want to use default as well
if custom_msgs: if custom_msgs:
if default_on or default_on is None: if default_on or default_on is None:
@ -289,46 +256,106 @@ class Interaction:
fmt = random.SystemRandom().choice(msgs) fmt = random.SystemRandom().choice(msgs)
# Due to our previous checks, the ID should only be in the dictionary once, in the current battle we're checking # Due to our previous checks, the ID should only be in the dictionary once, in the current battle we're checking
self.battling_off(player2=ctx.message.author) self.battling_off(battle)
await self.bot.br.update()
# Randomize the order of who is printed/sent to the update system # Randomize the order of who is printed/sent to the update system
if random.SystemRandom().randint(0, 1): winner, loser = battle.choose()
winner = battleP1
loser = battleP2 member_list = [m.id for m in ctx.guild.members]
query = """
SELECT id, rank, battle_rating, battle_wins, battle_losses
FROM
(SELECT
id,
ROW_NUMBER () OVER (ORDER BY battle_rating DESC) as "rank",
battle_rating,
battle_wins,
battle_losses
FROM
users
WHERE
id = any($1::bigint[]) AND
battle_rating IS NOT NULL
) AS sub
WHERE id = any($2)
"""
results = await self.bot.db.fetch(query, member_list, [winner.id, loser.id])
old_winner = old_loser = None
for result in results:
if result['id'] == loser.id:
old_loser = result
else:
old_winner = result
winner_rating, loser_rating, = utils.update_rating(
old_winner["battle_rating"] if old_winner else 1000,
old_loser["battle_rating"] if old_loser else 1000,
)
print(old_winner, old_loser)
update_query = """
UPDATE
users
SET
battle_rating = $1,
battle_wins = $2,
battle_losses = $3
WHERE
id=$4
"""
insert_query = """
INSERT INTO
users (id, battle_rating, battle_wins, battle_losses)
VALUES
($1, $2, $3, $4)
"""
if old_loser:
await self.bot.db.execute(
update_query,
loser_rating,
old_loser['battle_wins'],
old_loser['battle_losses'] + 1,
loser.id
)
else: else:
winner = battleP2 await self.bot.db.execute(insert_query, loser.id, loser_rating, 0, 1)
loser = battleP1 if old_winner:
await self.bot.db.execute(
update_query,
winner_rating,
old_winner['battle_wins'] + 1,
old_winner['battle_losses'] ,
winner.id
)
else:
await self.bot.db.execute(insert_query, winner.id, winner_rating, 1, 0)
msg = await ctx.send(fmt.format(winner=winner.display_name, loser=loser.display_name)) results = await self.bot.db.fetch(query, member_list, [winner.id, loser.id])
old_winner_rank, _ = self.bot.br.get_server_rank(winner) print(results)
old_loser_rank, _ = self.bot.br.get_server_rank(loser)
# Update our records; this will update our cache new_winner_rank = new_loser_rank = None
await utils.update_records('battle_records', self.bot.db, winner, loser) for result in results:
# Now wait a couple seconds to ensure cache is updated if result['id'] == loser.id:
await asyncio.sleep(2) new_loser_rank = result['rank']
await self.bot.br.update() else:
new_winner_rank = result['rank']
# Now get the new ranks after this stuff has been updated fmt = fmt.format(winner=winner.display_name, loser=loser.display_name)
new_winner_rank, _ = self.bot.br.get_server_rank(winner) if old_winner:
new_loser_rank, _ = self.bot.br.get_server_rank(loser)
fmt = msg.content
if old_winner_rank:
fmt += "\n{} - Rank: {} ( +{} )".format( fmt += "\n{} - Rank: {} ( +{} )".format(
winner.display_name, new_winner_rank, old_winner_rank - new_winner_rank winner.display_name, new_winner_rank, old_winner["rank"] - new_winner_rank
) )
else: else:
fmt += "\n{} - Rank: {}".format(winner.display_name, new_winner_rank) fmt += "\n{} - Rank: {}".format(winner.display_name, new_winner_rank)
if old_loser_rank: if old_winner:
fmt += "\n{} - Rank: {} ( -{} )".format(loser.display_name, new_loser_rank, new_loser_rank - old_loser_rank) fmt += "\n{} - Rank: {} ( -{} )".format(
loser.display_name, new_loser_rank, new_loser_rank - old_winner["rank"]
)
else: else:
fmt += "\n{} - Rank: {}".format(loser.display_name, new_loser_rank) fmt += "\n{} - Rank: {}".format(loser.display_name, new_loser_rank)
try: await ctx.send(fmt)
await msg.edit(content=fmt)
except Exception:
pass
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -340,21 +367,13 @@ class Interaction:
RESULT: You chicken out""" RESULT: You chicken out"""
# This is a check to make sure that the author is the one being BATTLED # This is a check to make sure that the author is the one being BATTLED
# And not the one that started the battle # And not the one that started the battle
battle = self.get_battle(ctx.message.author) battle = self.get_receivers_battle(ctx.author)
if battle is None: if battle is None:
await ctx.send("You are not currently being challenged to a battle!") await ctx.send("You are not currently being challenged to a battle!")
return return
battleP1 = discord.utils.find(lambda m: m.id == battle['p1'], ctx.message.guild.members) self.battling_off(battle)
if battleP1 is None: await ctx.send("{} has chickened out! What a loser~".format(ctx.author.mention))
await ctx.send("The person who challenged you to a battle has apparently left the server....why?")
return
battleP2 = ctx.message.author
# There's no need to update the stats for the members if they declined the battle
self.battling_off(player2=battleP2)
await ctx.send("{} has chickened out! What a loser~".format(battleP2.mention))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -365,7 +384,7 @@ class Interaction:
EXAMPLE: !boop @OtherPerson EXAMPLE: !boop @OtherPerson
RESULT: You do a boop o3o""" RESULT: You do a boop o3o"""
booper = ctx.message.author booper = ctx.author
if boopee is None: if boopee is None:
ctx.command.reset_cooldown(ctx) ctx.command.reset_cooldown(ctx)
await ctx.send("You try to boop the air, the air boops back. Be afraid....") await ctx.send("You try to boop the air, the air boops back. Be afraid....")
@ -382,64 +401,40 @@ class Interaction:
await ctx.send("Why the heck are you booping me? Get away from me >:c") await ctx.send("Why the heck are you booping me? Get away from me >:c")
return return
key = str(booper.id) query = "SELECT amount FROM boops WHERE booper = $1 AND boopee = $2"
boops = self.bot.db.load('boops', key=key, pluck='boops') or {} amount = await self.bot.db.fetchrow(query, booper.id, boopee.id)
amount = boops.get(str(boopee.id), 0) + 1 if amount is None:
entry = { amount = 1
'member_id': str(booper.id), replacement_query = "INSERT INTO boops (booper, boopee, amount) VALUES($1, $2, $3)"
'boops': { else:
str(boopee.id): amount replacement_query = "UPDATE boops SET amount=$3 WHERE booper=$1 AND boopee=$2"
} amount = amount['amount'] + 1
}
await self.bot.db.save('boops', entry)
fmt = "{0.mention} has just booped {1.mention}{3}! That's {2} times now!" await ctx.send(f"{booper.mention} has just booped {boopee.mention}{message}! That's {amount} times now!")
await ctx.send(fmt.format(booper, boopee, amount, message)) await self.bot.db.execute(replacement_query, booper.id, boopee.id, amount)
# noinspection PyMethodMayBeStatic class Battle:
class BattleRankings:
def __init__(self, bot):
self.db = bot.db
self.loop = bot.loop
self.ratings = None
def build_dict(self, seq, key): def __init__(self, initiator, receiver):
return dict((d[key], dict(d, rank=index + 1)) for (index, d) in enumerate(seq[::-1])) self.initiator = initiator
self.receiver = receiver
self.rand = random.SystemRandom()
def update_start(self): def is_initiator(self, player):
self.loop.create_task(self.update()) return player.id == self.initiator.id and player.guild.id == self.initiator.guild.id
async def update(self): def is_receiver(self, player):
ratings = await self.db.query(r.table('battle_records').order_by('rating')) return player.id == self.receiver.id and player.guild.id == self.receiver.guild.id
# Create a dictionary so that we have something to "get" from easily def is_battling(self, player):
self.ratings = self.build_dict(ratings, 'member_id') return self.is_initiator(player) or self.is_receiver(player)
def get_record(self, member): def choose(self):
data = self.ratings.get(str(member.id), {}) """Returns the two users in the order winner, loser"""
fmt = "{} - {}".format(data.get('wins'), data.get('losses')) choices = [self.initiator, self.receiver]
return fmt self.rand.shuffle(choices)
return choices
def get_rating(self, member):
data = self.ratings.get(str(member.id), {})
return data.get('rating')
def get_rank(self, member):
data = self.ratings.get(str(member.id), {})
return data.get('rank'), len(self.ratings)
def get_server_rank(self, member):
# Get the id's of all the members to compare to
server_ids = [str(m.id) for m in member.guild.members]
# Get all the ratings for members in this server
ratings = [x for x in self.ratings.values() if x['member_id'] in server_ids]
# Since we went from a dictionary to a list, we're no longer sorted, sort this
ratings = sorted(ratings, key=lambda x: x['rating'])
# Build our dictionary to get correct rankings
server_ratings = self.build_dict(ratings, 'member_id')
# Return the rank
return server_ratings.get(str(member.id), {}).get('rank'), len(server_ratings)
def setup(bot): def setup(bot):

View file

@ -22,12 +22,10 @@ class Links:
EXAMPLE: !g Random cat pictures! EXAMPLE: !g Random cat pictures!
RESULT: Links to sites with random cat pictures!""" RESULT: Links to sites with random cat pictures!"""
await ctx.message.channel.trigger_typing()
url = "https://www.google.com/search" url = "https://www.google.com/search"
# Turn safe filter on or off, based on whether or not this is a nsfw channel # Turn safe filter on or off, based on whether or not this is a nsfw channel
nsfw = await utils.channel_is_nsfw(ctx.message.channel, self.bot.db) nsfw = utils.channel_is_nsfw(ctx.message.channel)
safe = 'off' if nsfw else 'on' safe = 'off' if nsfw else 'on'
params = {'q': query, params = {'q': query,
@ -76,8 +74,6 @@ class Links:
EXAMPLE: !youtube Cat videos! EXAMPLE: !youtube Cat videos!
RESULT: Cat videos!""" RESULT: Cat videos!"""
await ctx.message.channel.trigger_typing()
key = utils.youtube_key key = utils.youtube_key
url = "https://www.googleapis.com/youtube/v3/search" url = "https://www.googleapis.com/youtube/v3/search"
params = {'key': key, params = {'key': key,
@ -111,8 +107,6 @@ class Links:
EXAMPLE: !wiki Test EXAMPLE: !wiki Test
RESULT: A link to the wikipedia article for the word test""" RESULT: A link to the wikipedia article for the word test"""
await ctx.message.channel.trigger_typing()
# All we need to do is search for the term provided, so the action, list, and format never need to change # All we need to do is search for the term provided, so the action, list, and format never need to change
base_url = "https://en.wikipedia.org/w/api.php" base_url = "https://en.wikipedia.org/w/api.php"
params = {"action": "query", params = {"action": "query",
@ -150,9 +144,7 @@ class Links:
EXAMPLE: !urban a normal phrase EXAMPLE: !urban a normal phrase
RESULT: Probably something lewd; this is urban dictionary we're talking about""" RESULT: Probably something lewd; this is urban dictionary we're talking about"""
if await utils.channel_is_nsfw(ctx.message.channel, self.bot.db): if utils.channel_is_nsfw(ctx.message.channel):
await ctx.message.channel.trigger_typing()
url = "http://api.urbandictionary.com/v0/define" url = "http://api.urbandictionary.com/v0/define"
params = {"term": msg} params = {"term": msg}
try: try:

View file

@ -11,6 +11,33 @@ import datetime
import psutil import psutil
def _command_signature(cmd):
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'[{name}...]')
else:
result.append(f'<{name}>')
return ' '.join(result)
class Miscallaneous: class Miscallaneous:
"""Core commands, these are the miscallaneous commands that don't fit into other categories'""" """Core commands, these are the miscallaneous commands that don't fit into other categories'"""
@ -19,32 +46,6 @@ class Miscallaneous:
self.process = psutil.Process() self.process = psutil.Process()
self.process.cpu_percent() self.process.cpu_percent()
def _command_signature(self, cmd):
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'[{name}...]')
else:
result.append(f'<{name}>')
return ' '.join(result)
@commands.command() @commands.command()
@commands.cooldown(1, 3, commands.cooldowns.BucketType.user) @commands.cooldown(1, 3, commands.cooldowns.BucketType.user)
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
@ -75,7 +76,8 @@ class Miscallaneous:
if entity: if entity:
entity = self.bot.get_cog(entity) or self.bot.get_command(entity) entity = self.bot.get_cog(entity) or self.bot.get_command(entity)
if entity is None: if entity is None:
fmt = "Hello! Here is a list of the sections of commands that I have (there are a lot of commands so just start with the sections...I know, I'm pretty great)\n" fmt = "Hello! Here is a list of the sections of commands that I have " \
"(there are a lot of commands so just start with the sections...I know, I'm pretty great)\n"
fmt += "To use a command's paramaters, you need to know the notation for them:\n" fmt += "To use a command's paramaters, you need to know the notation for them:\n"
fmt += "\t<argument> This means the argument is __**required**__.\n" fmt += "\t<argument> This means the argument is __**required**__.\n"
fmt += "\t[argument] This means the argument is __**optional**__.\n" fmt += "\t[argument] This means the argument is __**optional**__.\n"
@ -96,7 +98,7 @@ class Miscallaneous:
else: else:
chunks[len(chunks) - 1] += tmp chunks[len(chunks) - 1] += tmp
elif isinstance(entity, (commands.core.Command, commands.core.Group)): elif isinstance(entity, (commands.core.Command, commands.core.Group)):
tmp = "**{}**".format(self._command_signature(entity)) tmp = "**{}**".format(_command_signature(entity))
tmp += "\n{}".format(entity.help) tmp += "\n{}".format(entity.help)
chunks.append(tmp) chunks.append(tmp)
else: else:

View file

@ -46,15 +46,13 @@ class Osu:
async def get_users(self): async def get_users(self):
"""A task used to 'cache' all member's and their Osu profile's""" """A task used to 'cache' all member's and their Osu profile's"""
data = await self.bot.db.actual_load('osu') query = "SELECT id, osu FROM users WHERE osu IS NOT NULL;"
if data is None: rows = await self.bot.db.fetch(query)
return
for result in data: for row in rows:
member = int(result['member_id']) user = await self.get_user_from_api(row['osu'])
user = await self.get_user_from_api(result['osu_username'])
if user: if user:
self.osu_users[member] = user self.osu_users[row['id']] = user
@commands.group(invoke_without_command=True) @commands.group(invoke_without_command=True)
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
@ -63,7 +61,7 @@ class Osu:
EXAMPLE: !osu @Person EXAMPLE: !osu @Person
RESULT: Informationa bout that person's osu account""" RESULT: Informationa bout that person's osu account"""
await ctx.message.channel.trigger_typing()
if member is None: if member is None:
member = ctx.message.author member = ctx.message.author
@ -95,21 +93,19 @@ class Osu:
EXAMPLE: !osu add username EXAMPLE: !osu add username
RESULT: Links your username to your account, and allows stats to be pulled from it""" RESULT: Links your username to your account, and allows stats to be pulled from it"""
await ctx.message.channel.trigger_typing()
author = ctx.message.author author = ctx.message.author
user = await self.get_user(author, username) user = await self.get_user(author, username)
if user is None: if user is None:
await ctx.send("I couldn't find an osu user that matches {}".format(username)) await ctx.send("I couldn't find an osu user that matches {}".format(username))
return return
entry = {
'member_id': str(author.id),
'osu_username': user.username
}
await self.bot.db.save('osu', entry)
await ctx.send("I have just saved your Osu user {}".format(author.display_name)) await ctx.send("I have just saved your Osu user {}".format(author.display_name))
update = {
"id": author.id,
"osu": user.username
}
await self.bot.db.upsert("users", update)
@osu.command(name='score', aliases=['scores']) @osu.command(name='score', aliases=['scores'])
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
@ -119,7 +115,7 @@ class Osu:
EXAMPLE: !osu scores @Person 5 EXAMPLE: !osu scores @Person 5
RESULT: The top 5 maps for the user @Person""" RESULT: The top 5 maps for the user @Person"""
await ctx.message.channel.trigger_typing()
# Set the defaults before we go through our passed data to figure out what we want # Set the defaults before we go through our passed data to figure out what we want
limit = 5 limit = 5
member = ctx.message.author member = ctx.message.author
@ -135,7 +131,7 @@ class Osu:
limit = 50 limit = 50
elif limit < 1: elif limit < 1:
limit = 5 limit = 5
except: except Exception:
converter = commands.converter.MemberConverter() converter = commands.converter.MemberConverter()
try: try:
member = await converter.convert(ctx, piece) member = await converter.convert(ctx, piece)

View file

@ -38,8 +38,6 @@ class Overwatch:
EXAMPLE: !ow stats @OtherPerson Junkrat EXAMPLE: !ow stats @OtherPerson Junkrat
RESULT: Whether or not you should unfriend this person because they're a dirty rat""" RESULT: Whether or not you should unfriend this person because they're a dirty rat"""
await ctx.message.channel.trigger_typing()
user = user or ctx.message.author user = user or ctx.message.author
bt = self.bot.db.load('overwatch', key=str(user.id), pluck='battletag') bt = self.bot.db.load('overwatch', key=str(user.id), pluck='battletag')
@ -99,7 +97,7 @@ class Overwatch:
EXAMPLE: !ow add Username#1234 EXAMPLE: !ow add Username#1234
RESULT: Your battletag is now saved""" RESULT: Your battletag is now saved"""
await ctx.message.channel.trigger_typing()
# Battletags are normally provided like name#id # Battletags are normally provided like name#id
# However the API needs this to be a -, so repliace # with - if it exists # However the API needs this to be a -, so repliace # with - if it exists

View file

@ -1,21 +1,33 @@
import asyncio import asyncio
import discord import discord
import re
import traceback import traceback
from discord.ext import commands
import utils import utils
BASE_URL = 'https://api.picarto.tv/v1' BASE_URL = 'https://api.picarto.tv/v1'
def produce_embed(*channels):
description = ""
# Loop through each channel and produce the information that will go in the description
for channel in channels:
url = f"https://picarto.tv/{channel.get('name')}"
description = f"""{description}\n\n**Title:** [{channel.get("title")}]({url})
**Channel:** [{channel.get("name")}]({url})
**Adult:** {"Yes" if channel.get("adult") else "No"}
**Gaming:** {"Yes" if channel.get("gaming") else "No"}
**Commissions:** {"Yes" if channel.get("commissions") else "No"}"""
return discord.Embed(title="Channels that have gone online!", description=description.strip())
class Picarto: class Picarto:
"""Pretty self-explanatory""" """Pretty self-explanatory"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.task = self.bot.loop.create_task(self.picarto_task()) self.task = self.bot.loop.create_task(self.picarto_task())
self.channel_info = {}
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
async def get_online_users(self): async def get_online_users(self):
@ -25,49 +37,38 @@ class Picarto:
'adult': 'true', 'adult': 'true',
'gaming': 'true' 'gaming': 'true'
} }
self.online_channels = await utils.request(url, payload=payload) channel_info = {}
channels = await utils.request(url, payload=payload)
if channels:
for channel in channels:
name = channel["name"]
previous = self.channel_info.get("name")
# There are three statuses, on, remained, and off
# On means they were off previously, but are now online
# Remained means they were on previous, and are still on
# Off means they were on preivous, but are now offline
# If they weren't included in the online channels...well they're off
if previous is None:
channel_info[name] = channel
channel_info[name]["status"] = "on"
elif previous["status"] in ["on", "remaining"]:
channel_info[name] = channel
channel_info[name]["status"] = "remaining"
# After loop has finished successfully, we want to override the statuses of the channels
self.channel_info = channel_info
async def channel_embed(self, channel): def produce_embed(self, *channels):
# First make sure the picarto URL is actually given description = ""
if not channel: # Loop through each channel and produce the information that will go in the description
return None for channel in channels:
# Use regex to get the actual username so that we can make a request to the API url = f"https://picarto.tv/{channel.get('name')}"
stream = re.search("(?<=picarto.tv/)(.*)", channel).group(1) description = f"""{description}\n\n**Title:** [{channel.get("title")}]({url})
url = BASE_URL + '/channel/name/{}'.format(stream) **Channel:** [{channel.get("name")}]({url})
**Adult:** {"Yes" if channel.get("adult") else "No"}
**Gaming:** {"Yes" if channel.get("gaming") else "No"}
**Commissions:** {"Yes" if channel.get("commissions") else "No"}"""
data = await utils.request(url) return discord.Embed(title="Channels that have gone online!", description=description.strip())
if data is None:
return None
# Not everyone has all these settings, so use this as a way to print information if it does, otherwise ignore it
things_to_print = ['comissions', 'adult', 'followers', 'category', 'online']
embed = discord.Embed(title='{}\'s Picarto'.format(data['name']), url=channel)
avatar_url = 'https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg'.format(data['name'].lower())
embed.set_thumbnail(url=avatar_url)
for i, result in data.items():
if i in things_to_print and str(result):
i = i.title().replace('_', ' ')
embed.add_field(name=i, value=str(result))
# Social URL's can be given if a user wants them to show
# Print them if they exist, otherwise don't try to include them
social_links = data.get('social_urls', {})
for i, result in social_links.items():
embed.add_field(name=i.title(), value=result)
return embed
def channel_online(self, channel):
# Channel is the name we are checking against that
# This creates a list of all users that match this channel name (should only ever be 1)
# And returns True as long as it is more than 0
if not self.online_channels or channel is None:
return False
channel = re.search("(?<=picarto.tv/)(.*)", channel).group(1)
return channel.lower() in [stream['name'].lower() for stream in self.online_channels]
async def picarto_task(self): async def picarto_task(self):
try: try:
@ -82,237 +83,34 @@ class Picarto:
await asyncio.sleep(30) await asyncio.sleep(30)
async def check_channels(self): async def check_channels(self):
query = """
SELECT
id, followed_picarto_channels, COALESCE(picarto_alerts, default_alerts) AS channel
FROM
guilds
WHERE
COALESCE(picarto_alerts, default_alerts) IS NOT NULL
"""
# Recheck who is currently online
await self.get_online_users() await self.get_online_users()
picarto = await self.bot.db.actual_load('picarto', table_filter={'notifications_on': 1}) # Now get all guilds and their picarto channels they follow and loop through them
for data in picarto: results = await self.bot.db.fetch(query) or []
m_id = int(data['member_id']) for result in results:
url = data['picarto_url'] # Get all the channels that have gone online
# Check if they are online gone_online = [
online = self.channel_online(url) self.channel_info.get(name)
# If they're currently online, but saved as not then we'll let servers know they are now online for name in result["followed_picarto_channels"]
if online and data['live'] == 0: if self.channel_info.get(name) == "on"
msg = "{member.display_name} has just gone live!" ]
await self.bot.db.save('picarto', {'live': 1, 'member_id': str(m_id)}) # If they've gone online, produce the embed for them and send it
# Otherwise our notification will say they've gone offline if gone_online:
elif not online and data['live'] == 1: embed = produce_embed(*gone_online)
msg = "{member.display_name} has just gone offline!" channel = self.bot.get_channel(result["channel"])
await self.bot.db.save('picarto', {'live': 0, 'member_id': str(m_id)}) if channel is not None:
else: try:
continue await channel.send(embed=embed)
except (discord.Forbidden, discord.HTTPException, AttributeError):
embed = await self.channel_embed(url) pass
# Loop through each server that they are set to notify
for s_id in data['servers']:
server = self.bot.get_guild(int(s_id))
# If we can't find it, ignore this one
if server is None:
continue
member = server.get_member(m_id)
# If we can't find them in this server, also ignore
if member is None:
continue
# Get the notifications settings, get the picarto setting
notifications = self.bot.db.load('server_settings', key=s_id, pluck='notifications') or {}
# Set our default to either the one set, or the default channel of the server
default_channel_id = notifications.get('default')
# If it is has been overriden by picarto notifications setting, use this
channel_id = notifications.get('picarto') or default_channel_id
# Now get the channel
if channel_id:
channel = server.get_channel(int(channel_id))
else:
continue
# Then just send our message
try:
await channel.send(msg.format(member=member), embed=embed)
except (discord.Forbidden, discord.HTTPException, AttributeError):
pass
@commands.group(invoke_without_command=True)
@utils.can_run(send_messages=True)
async def picarto(self, ctx, member: discord.Member = None):
"""This command can be used to view Picarto stats about a certain member
EXAMPLE: !picarto @otherPerson
RESULT: Info about their picarto stream"""
await ctx.message.channel.trigger_typing()
# If member is not given, base information on the author
member = member or ctx.message.author
member_url = self.bot.db.load('picarto', key=member.id, pluck='picarto_url')
if member_url is None:
await ctx.send("That user does not have a picarto url setup!")
return
embed = await self.channel_embed(member_url)
await ctx.send(embed=embed)
@picarto.command(name='add')
@commands.guild_only()
@utils.can_run(send_messages=True)
async def add_picarto_url(self, ctx, url: str):
"""Saves your user's picarto URL
EXAMPLE: !picarto add MyUsername
RESULT: Your picarto stream is saved, and notifications should go to this guild"""
await ctx.message.channel.trigger_typing()
# This uses a lookbehind to check if picarto.tv exists in the url given
# If it does, it matches picarto.tv/user and sets the url as that
# Then (in the else) add https://www. to that
# Otherwise if it doesn't match, we'll hit an AttributeError due to .group(0)
# This means that the url was just given as a user (or something complete invalid)
# So set URL as https://www.picarto.tv/[url]
# Even if this was invalid such as https://www.picarto.tv/picarto.tv/user
# For example, our next check handles that
try:
url = re.search("((?<=://)?picarto.tv/)+(.*)", url).group(0)
except AttributeError:
url = "https://www.picarto.tv/{}".format(url)
else:
url = "https://www.{}".format(url)
channel = re.search("https://www.picarto.tv/(.*)", url).group(1)
api_url = BASE_URL + '/channel/name/{}'.format(channel)
data = await utils.request(api_url)
if not data:
await ctx.send("That Picarto user does not exist! What would be the point of adding a nonexistant Picarto "
"user? Silly")
return
key = str(ctx.message.author.id)
# Check if it exists first, if it does we don't want to override some of the settings
result = self.bot.db.load('picarto', key=key)
if result:
entry = {
'picarto_url': url,
'member_id': key
}
else:
entry = {
'picarto_url': url,
'servers': [str(ctx.message.guild.id)],
'notifications_on': 1,
'live': 0,
'member_id': key
}
await self.bot.db.save('picarto', entry)
await ctx.send(
"I have just saved your Picarto URL {}, this guild will now be notified when you go live".format(
ctx.message.author.mention))
@picarto.command(name='remove', aliases=['delete'])
@utils.can_run(send_messages=True)
async def remove_picarto_url(self, ctx):
"""Removes your picarto URL"""
key = str(ctx.message.author.id)
result = self.bot.db.load('picarto', key=key)
if result:
entry = {
'picarto_url': None,
'member_id': str(ctx.message.author.id)
}
await self.bot.db.save('picarto', entry)
await ctx.send("I am no longer saving your picarto URL {}".format(ctx.message.author.mention))
else:
await ctx.send("I cannot remove something that I don't have (you've never saved your Picarto URL)")
@picarto.command(name='alerts')
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def picarto_alerts_channel(self, ctx, channel: discord.TextChannel):
"""Sets the notifications channel for picarto notifications
EXAMPLE: !picarto alerts #picarto
RESULT: Picarto notifications will go to this channel
"""
entry = {
'server_id': str(ctx.message.guild.id),
'notifications': {
'picarto': str(channel.id)
}
}
await self.bot.db.save('server_settings', entry)
await ctx.send("All Picarto notifications will now go to {}".format(channel.mention))
@picarto.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def notify(self, ctx):
"""This can be used to turn picarto notifications on or off
Call this command by itself, to add this guild to the list of guilds to be notified
EXAMPLE: !picarto notify
RESULT: This guild will now be notified of you going live"""
key = str(ctx.message.author.id)
servers = self.bot.db.load('picarto', key=key, pluck='servers')
# Check if this user is saved at all
if servers is None:
await ctx.send(
"I do not have your Picarto URL added {}. You can save your Picarto url with !picarto add".format(
ctx.message.author.mention))
# Then check if this guild is already added as one to notify in
elif str(ctx.message.guild.id) in servers:
await ctx.send("I am already set to notify in this guild...")
else:
servers.append(str(ctx.message.guild.id))
entry = {
'member_id': key,
'servers': servers
}
await self.bot.db.save('picarto', entry)
await ctx.send("This server will now be notified if you go live")
@notify.command(name='on', aliases=['start,yes'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def notify_on(self, ctx):
"""Turns picarto notifications on
EXAMPLE: !picarto notify on
RESULT: Notifications are sent when you go live"""
key = str(ctx.message.author.id)
result = self.bot.db.load('picarto', key=key)
if result:
entry = {
'member_id': key,
'notifications_on': 1
}
await self.bot.db.save('picarto', entry)
await ctx.send("I will notify if you go live {}, you'll get a bajillion followers I promise c:".format(
ctx.message.author.mention))
else:
await ctx.send("I can't notify if you go live if I don't know your picarto URL yet!")
@notify.command(name='off', aliases=['stop,no'])
@commands.guild_only()
@utils.can_run(send_messages=True)
async def notify_off(self, ctx):
"""Turns picarto notifications off
EXAMPLE: !picarto notify off
RESULT: No more notifications sent when you go live"""
key = str(ctx.message.author.id)
result = self.bot.db.load('picarto', key=key)
if result:
entry = {
'member_id': key,
'notifications_on': 0
}
await self.bot.db.save('picarto', entry)
await ctx.send(
"I will not notify if you go live anymore {}, "
"are you going to stream some lewd stuff you don't want people to see?~".format(
ctx.message.author.mention))
else:
await ctx.send(
"I'm already not going to notify anyone, because I don't have your picarto URL saved...")
def setup(bot): def setup(bot):

View file

@ -1,13 +1,12 @@
from discord.ext import commands from discord.ext import commands
import discord from collections import defaultdict
import utils import utils
import random import discord
import pendulum
import re import re
import asyncio import asyncio
import traceback import random
class Raffle: class Raffle:
@ -15,174 +14,57 @@ class Raffle:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.bot.loop.create_task(self.raffle_task()) self.raffles = defaultdict(list)
async def raffle_task(self): def create_raffle(self, ctx, title, num):
while True: raffle = GuildRaffle(ctx, title, num)
try: self.raffles[ctx.guild.id].append(raffle)
await self.check_raffles() raffle.start()
except Exception as error:
with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f)
finally:
await asyncio.sleep(60)
async def check_raffles(self): @commands.command(name="raffles")
# This is used to periodically check the current raffles, and see if they have ended yet
# If the raffle has ended, we'll pick a winner from the entrants
raffles = self.bot.db.load('raffles')
if raffles is None:
return
for server_id, raffle in raffles.items():
server = self.bot.get_guild(int(server_id))
# Check to see if this cog can find the server in question
if server is None:
continue
for r in raffle['raffles']:
title = r['title']
entrants = r['entrants']
now = pendulum.now(tz="UTC")
expires = pendulum.parse(r['expires'])
# Now lets compare and see if this raffle has ended, if not just continue
if expires > now:
continue
# Make sure there are actually entrants
if len(entrants) == 0:
fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title)
else:
winner = None
count = 0
while winner is None:
winner = server.get_member(int(random.SystemRandom().choice(entrants)))
# Lets make sure we don't get caught in an infinite loop
# Realistically having more than 50 random entrants found that aren't in the server anymore
# Isn't something that should be an issue, but better safe than sorry
count += 1
if count >= 50:
break
if winner is None:
fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format(
title)
else:
fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name)
# Get the notifications settings, get the raffle setting
notifications = self.bot.db.load('server_settings', key=server.id, pluck='notifications') or {}
# Set our default to either the one set
default_channel_id = notifications.get('default')
# If it is has been overriden by picarto notifications setting, use this
channel_id = notifications.get('raffle') or default_channel_id
if channel_id:
channel = self.bot.get_channel(int(channel_id))
else:
continue
try:
await channel.send(fmt)
except (discord.Forbidden, AttributeError):
pass
# No matter which one of these matches were met, the raffle has ended and we want to remove it
raffle['raffles'].remove(r)
entry = {
'server_id': raffle['server_id'],
'raffles': raffle['raffles']
}
await self.bot.db.save('raffles', entry)
@commands.command()
@commands.guild_only() @commands.guild_only()
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
async def raffles(self, ctx): async def _raffles(self, ctx):
"""Used to print the current running raffles on the server """Used to print the current running raffles on the server
EXAMPLE: !raffles EXAMPLE: !raffles
RESULT: A list of the raffles setup on this server""" RESULT: A list of the raffles setup on this server"""
raffles = self.bot.db.load('raffles', key=ctx.message.guild.id, pluck='raffles') raffles = self.raffles[ctx.guild.id]
if not raffles: if len(raffles) == 0:
await ctx.send("There are currently no raffles setup on this server!") await ctx.send("There are currently no raffles setup on this server!")
return return
# For EVERY OTHER COG, when we get one result, it is nice to have it return that exact object embed = discord.Embed(title=f"Raffles in {ctx.guild.name}")
# This is the only cog where that is different, so just to make this easier lets throw it
# back in a one-indexed list, for easier parsing for num, raffle in enumerate(raffles):
if isinstance(raffles, dict): embed.add_field(
raffles = [raffles] name=f"Raffle {num + 1}",
fmt = "\n\n".join("**Raffle:** {}\n**Title:** {}\n**Total Entrants:** {}\n**Ends:** {} UTC".format( value=f"Title: {raffle.title}\n"
num + 1, f"Total Entrants: {len(raffle.entrants)}\n"
raffle['title'], f"Ends in {raffle.remaining}",
len(raffle['entrants']), inline=False
raffle['expires']) for num, raffle in enumerate(raffles)) )
await ctx.send(fmt) await ctx.send(embed=embed)
@commands.group(invoke_without_command=True) @commands.group(invoke_without_command=True)
@commands.guild_only() @commands.guild_only()
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
async def raffle(self, ctx, raffle_id: int = 0): async def raffle(self, ctx, raffle_id: int):
"""Used to enter a raffle running on this server """Used to enter a raffle running on this server
If there is more than one raffle running, provide an ID of the raffle you want to enter If there is more than one raffle running, provide an ID of the raffle you want to enter
EXAMPLE: !raffle 1 EXAMPLE: !raffle 1
RESULT: You've entered the first raffle!""" RESULT: You've entered the first raffle!"""
# Lets let people use 1 - (length of raffles) and handle 0 base ourselves try:
raffle_id -= 1 raffle = self.raffles[ctx.guild.id][raffle_id - 1]
author = ctx.message.author except IndexError:
key = str(ctx.message.guild.id) await ctx.send(f"I could not find a raffle for ID {raffle_id}")
await self._raffles.invoke(ctx)
raffles = self.bot.db.load('raffles', key=key, pluck='raffles')
if raffles is None:
await ctx.send("There are currently no raffles setup on this server!")
return
raffle_count = len(raffles)
# There is only one raffle, so use the first's info
if raffle_count == 1:
entrants = raffles[0]['entrants']
# Lets make sure that the user hasn't already entered the raffle
if str(author.id) in entrants:
await ctx.send("You have already entered this raffle!")
return
entrants.append(str(author.id))
update = {
'raffles': raffles,
'server_id': key
}
await self.bot.db.save('raffles', update)
await ctx.send("{} you have just entered the raffle!".format(author.mention))
# Otherwise, make sure the author gave a valid raffle_id
elif raffle_id in range(raffle_count):
entrants = raffles[raffle_id]['entrants']
# Lets make sure that the user hasn't already entered the raffle
if str(author.id) in entrants:
await ctx.send("You have already entered this raffle!")
return
entrants.append(str(author.id))
# Since we have no good thing to filter things off of, lets use the internal rethinkdb id
update = {
'raffles': raffles,
'server_id': key
}
await self.bot.db.save('raffles', update)
await ctx.send("{} you have just entered the raffle!".format(author.mention))
else: else:
fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \ if raffle.enter(ctx.author):
"There are currently `{}` raffles running, use {}raffles to view the current running raffles".format( await ctx.send(f"You have just joined the raffle {raffle['title']}")
raffle_count, ctx.prefix else:
) await ctx.send("You have already entered this raffle!")
await ctx.send(fmt)
@raffle.command(name='create', aliases=['start', 'begin', 'add']) @raffle.command(name='create', aliases=['start', 'begin', 'add'])
@commands.guild_only() @commands.guild_only()
@ -193,10 +75,8 @@ class Raffle:
EXAMPLE: !raffle create EXAMPLE: !raffle create
RESULT: A follow-along for setting up a new raffle""" RESULT: A follow-along for setting up a new raffle"""
author = ctx.message.author author = ctx.author
server = ctx.message.guild channel = ctx.channel
channel = ctx.message.channel
now = pendulum.now(tz="UTC")
await ctx.send( await ctx.send(
"Ready to start a new raffle! Please respond with the title you would like to use for this raffle!") "Ready to start a new raffle! Please respond with the title you would like to use for this raffle!")
@ -212,13 +92,13 @@ class Raffle:
fmt = "Alright, your new raffle will be titled:\n\n{}\n\nHow long would you like this raffle to run for? " \ fmt = "Alright, your new raffle will be titled:\n\n{}\n\nHow long would you like this raffle to run for? " \
"The format should be [number] [length] for example, `2 days` or `1 hour` or `30 minutes` etc. " \ "The format should be [number] [length] for example, `2 days` or `1 hour` or `30 minutes` etc. " \
"The minimum for this is 10 minutes, and the maximum is 3 months" "The minimum for this is 10 minutes, and the maximum is 3 days"
await ctx.send(fmt.format(title)) await ctx.send(fmt.format(title))
# Our check to ensure that a proper length of time was passed # Our check to ensure that a proper length of time was passed
def check(m): def check(m):
if m.author == author and m.channel == channel: if m.author == author and m.channel == channel:
return re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", m.content.lower()) is not None return re.search("\d+ (minutes?|hours?|days?)", m.content.lower()) is not None
else: else:
return False return False
@ -229,73 +109,86 @@ class Raffle:
return return
# Lets get the length provided, based on the number and type passed # Lets get the length provided, based on the number and type passed
num, term = re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", msg.content.lower()).group(0).split(' ') num, term = re.search("(\d+) (minutes?|hours?|days?)", msg.content.lower()).groups()
# This should be safe to convert, we already made sure with our check earlier this would match # This should be safe to convert, we already made sure with our check earlier this would match
num = int(num) num = int(num)
# Now lets ensure this meets our min/max # Now lets ensure this meets our min/max
if "minute" in term and (num < 10 or num > 129600): if "minute" in term:
num = num * 60
elif "hour" in term:
num = num * 60 * 60
elif "day" in term:
num = num * 24 * 60 * 60
if 60 < num < 259200:
await ctx.send( await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months") "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 days")
return
elif "hour" in term and num > 2160:
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "day" in term and num > 90:
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "week" in term and num > 12:
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return
elif "month" in term and num > 3:
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
return return
# Pendulum only accepts the plural version of terms, lets make sure this is added self.create_raffle(ctx, title, num)
term = term if term.endswith('s') else term + 's'
# If we're in the range, lets just pack this in a dictionary we can pass to set the time we want, then set that
payload = {term: num}
expires = now.add(**payload)
# Now we're ready to add this as a new raffle
entry = {
'title': title,
'expires': expires.to_datetime_string(),
'entrants': [],
'author': str(author.id),
}
raffles = self.bot.db.load('raffles', key=server.id, pluck='raffles') or []
raffles.append(entry)
update = {
'server_id': str(server.id),
'raffles': raffles
}
await self.bot.db.save('raffles', update)
await ctx.send("I have just saved your new raffle!") await ctx.send("I have just saved your new raffle!")
@raffle.command(name='alerts')
@commands.guild_only()
@utils.can_run(manage_guild=True)
async def raffle_alerts_channel(self, ctx, channel: discord.TextChannel):
"""Sets the notifications channel for raffle notifications
EXAMPLE: !raffle alerts #raffle
RESULT: raffle notifications will go to this channel
"""
entry = {
'server_id': str(ctx.message.guild.id),
'notifications': {
'raffle': str(channel.id)
}
}
await self.bot.db.save('server_settings', entry)
await ctx.send("All raffle notifications will now go to {}".format(channel.mention))
def setup(bot): def setup(bot):
bot.add_cog(Raffle(bot)) bot.add_cog(Raffle(bot))
class GuildRaffle:
def __init__(self, ctx, title, expires):
self._ctx = ctx
self.title = title
self.expires = expires
self.entrants = set()
self.task = None
@property
def guild(self):
return self._ctx.guild
@property
def db(self):
return self._ctx.bot.db
def start(self):
self.task = self._ctx.bot.loop.call_later(self.expires, self.end_raffle())
@property
def remaining(self):
minutes, seconds = divmod(self.task.when(), 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
return f"{days} days, {hours} hours, {minutes} minutes, {seconds} seconds"
def enter(self, entrant):
self.entrants.add(entrant)
async def end_raffle(self):
entrants = {e for e in self.entrants if self.guild.get_member(e.id)}
query = """
SELECT
COALESCE(raffle_alerts, default_alerts) AS channel,
FROM
guilds
WHERE
id = $1
AND
COALESCE(raffle_alerts, default_alerts) IS NOT NULL
"""
channel = None
result = await self.db.fetch(query, self.guild.id)
if result:
channel = self.guild.get_channel(result['channel'])
if channel is None:
return
if entrants:
winner = random.SystemRandom().choice(self.entrants)
await channel.send(f"The winner of the raffle `{self.title}` is {winner.mention}! Congratulations!")
else:
await channel.send(
f"There were no entrants to the raffle `{self.title}`, who are in this server currently!"
)

View file

@ -85,7 +85,7 @@ class Roles:
total_members = len(role.members) total_members = len(role.members)
embed.add_field(name="Total members", value=str(total_members)) embed.add_field(name="Total members", value=str(total_members))
# If there are only a few members in this role, display them # If there are only a few members in this role, display them
if total_members <= 5 and total_members > 0: if 5 >= total_members > 0:
embed.add_field(name="Members", value="\n".join(m.display_name for m in role.members)) embed.add_field(name="Members", value="\n".join(m.display_name for m in role.members))
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:

View file

@ -299,7 +299,7 @@ class Game:
await p.show_table() await p.show_table()
await p.get_bid() await p.get_bid()
self.order_turns(self.get_highest_bidder()) self.order_turns(self.get_highest_bidder())
# Bids are complete, time to start the game # Bids are complete, time to start the game
await self.clean_messages() await self.clean_messages()
@ -319,13 +319,14 @@ class Game:
await self.update_table() await self.update_table()
# Get the winner after the round, increase their tricks # Get the winner after the round, increase their tricks
winner = self.get_round_winner() winner = self.get_round_winner()
winning_card = winner.played_card
winner.tricks += 1 winner.tricks += 1
# Order players based off the winner # Order players based off the winner
self.order_turns(winner) self.order_turns(winner)
# Reset the round # Reset the round
await self.reset_round() await self.reset_round()
fmt = "{} won with a {}".format(winner.discord_member.display_name, winner.played_card) fmt = "{} won with a {}".format(winner.discord_member.display_name, winning_card)
for p in self.players: for p in self.players:
await p.send_message(content=fmt) await p.send_message(content=fmt)
@ -352,9 +353,12 @@ class Game:
highest_bid = -1 highest_bid = -1
highest_player = None highest_player = None
for player in self.players: for player in self.players:
print(player.bid_num, player.discord_member.display_name)
if player.bid_num > highest_bid: if player.bid_num > highest_bid:
highest_player = player highest_player = player
print(highest_player.discord_member.display_name)
return highest_player return highest_player
def order_turns(self, player): def order_turns(self, player):
@ -432,7 +436,7 @@ class Spades:
# If so add the player to it # If so add the player to it
self.pending_game.join(author) self.pending_game.join(author)
# If we've hit 4 players, we want to start the game, add it to our list of games, and wipe our pending game # If we've hit 4 players, we want to start the game, add it to our list of games, and wipe our pending game
if len(self.pending_game.players) == 4: if len(self.pending_game.players) == 2:
task = self.bot.loop.create_task(self.pending_game.start()) task = self.bot.loop.create_task(self.pending_game.start())
self.games.append((self.pending_game, task)) self.games.append((self.pending_game, task))
self.pending_game = None self.pending_game = None

View file

@ -27,10 +27,10 @@ class Spotify:
async def api_token_task(self): async def api_token_task(self):
while True: while True:
delay = 2400
try: try:
delay = await self.get_api_token() delay = await self.get_api_token()
except Exception as error: except Exception as error:
delay = 2400
with open("error_log", 'a') as f: with open("error_log", 'a') as f:
traceback.print_tb(error.__traceback__, file=f) traceback.print_tb(error.__traceback__, file=f)
print('{0.__class__.__name__}: {0}'.format(error), file=f) print('{0.__class__.__name__}: {0}'.format(error), file=f)

View file

@ -92,8 +92,6 @@ class Stats:
EXAMPLE: !command stats play EXAMPLE: !command stats play
RESULT: The realization that this is the only reason people use me ;-;""" RESULT: The realization that this is the only reason people use me ;-;"""
await ctx.message.channel.trigger_typing()
cmd = self.bot.get_command(command) cmd = self.bot.get_command(command)
if cmd is None: if cmd is None:
await ctx.send("`{}` is not a valid command".format(command)) await ctx.send("`{}` is not a valid command".format(command))
@ -124,8 +122,6 @@ class Stats:
EXAMPLE: !command leaderboard me EXAMPLE: !command leaderboard me
RESULT: The realization of how little of a life you have""" RESULT: The realization of how little of a life you have"""
await ctx.message.channel.trigger_typing()
if re.search('(author|me)', option): if re.search('(author|me)', option):
mid = str(ctx.message.author.id) mid = str(ctx.message.author.id)
# First lets get all the command usage # First lets get all the command usage
@ -176,31 +172,35 @@ class Stats:
EXAMPLE: !mostboops EXAMPLE: !mostboops
RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!""" RESULT: You've booped @OtherPerson 351253897120935712093572193057310298 times!"""
query = """
SELECT
boopee, amount
FROM
boops
WHERE
booper=$1
AND
boopee IN ($2)
ORDER BY
amount DESC
LIMIT 1
"""
members = ", ".join(f"{m.id}" for m in ctx.guild.members)
most = await self.bot.db.fetchrow(query, ctx.author.id, members)
boops = self.bot.db.load('boops', key=ctx.message.author.id) boops = self.bot.db.load('boops', key=ctx.message.author.id)
if boops is None or "boops" not in boops: if boops is None or "boops" not in boops:
await ctx.send("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) await ctx.send("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention))
return return
# Just to make this easier, just pay attention to the boops data, now that we have the right entry if len(most) == 0:
boops = boops['boops'] await ctx.send(f"You have not booped anyone in this server {ctx.author.mention}")
sorted_boops = sorted(
((ctx.guild.get_member(int(member_id)), amount)
for member_id, amount in boops.items()
if ctx.guild.get_member(int(member_id))),
reverse=True,
key=lambda k: k[1]
)
# Since this is sorted, we just need to get the following information on the first user in the list
try:
member, most_boops = sorted_boops[0]
except IndexError:
await ctx.send("You have not booped anyone in this server {}".format(ctx.message.author.mention))
return
else: else:
await ctx.send("{0} you have booped {1} the most amount of times, coming in at {2} times".format( member = ctx.guild.get_member(most['boopee'])
ctx.message.author.mention, member.display_name, most_boops)) await ctx.send(
f"{ctx.author.mention} you have booped {member.display_name} the most amount of times, "
f"coming in at {most['amount']} times"
)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -210,28 +210,30 @@ class Stats:
EXAMPLE: !listboops EXAMPLE: !listboops
RESULT: The list of your booped members!""" RESULT: The list of your booped members!"""
await ctx.message.channel.trigger_typing()
boops = self.bot.db.load('boops', key=ctx.message.author.id) query = """
if not boops: SELECT
await ctx.send("You have not booped anyone {} Why the heck not...?".format(ctx.message.author.mention)) boopee, amount
return FROM
boops
WHERE
booper=$1
AND
boopee IN ($2)
ORDER BY
amount DESC
LIMIT 10
"""
# Just to make this easier, just pay attention to the boops data, now that we have the right entry members = ", ".join(f"{m.id}" for m in ctx.guild.members)
boops = boops['boops'] most = await self.bot.db.fetch(query, ctx.author.id, members)
sorted_boops = sorted( if len(most) != 0:
((ctx.guild.get_member(int(member_id)), amount)
for member_id, amount in boops.items()
if ctx.guild.get_member(int(member_id))),
reverse=True,
key=lambda k: k[1]
)
if sorted_boops:
embed = discord.Embed(title="Your booped victims", colour=ctx.author.colour) embed = discord.Embed(title="Your booped victims", colour=ctx.author.colour)
embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url) embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url)
for member, amount in sorted_boops: for row in most:
embed.add_field(name=member.display_name, value=amount) member = ctx.guild.get_member(row['boopee'])
embed.add_field(name=member.display_name, value=row['amount'])
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
await ctx.send("You haven't booped anyone in this server!") await ctx.send("You haven't booped anyone in this server!")
@ -244,35 +246,34 @@ class Stats:
EXAMPLE: !leaderboard EXAMPLE: !leaderboard
RESULT: A leaderboard of this server's battle records""" RESULT: A leaderboard of this server's battle records"""
await ctx.message.channel.trigger_typing()
# Create a list of the ID's of all members in this server, for comparison to the records saved query = """
server_member_ids = [member.id for member in ctx.message.guild.members] SELECT
battles = self.bot.db.load('battle_records') id, battle_rating
if battles is None or len(battles) == 0: FROM
users
WHERE
id = any($1::bigint[])
ORDER BY
battle_rating DESC
"""
results = await self.bot.db.fetch(query, [m.id for m in ctx.guild.members])
if len(results) == 0:
await ctx.send("No one has battled on this server!") await ctx.send("No one has battled on this server!")
else:
battles = [ output = []
battle for row in results:
for member_id, battle in battles.items() member = ctx.guild.get_member(row['id'])
if int(member_id) in server_member_ids output.append(f"{member.display_name} (Rating: {row['battle_rating']})")
]
# Sort the members based on their rating try:
sorted_members = sorted(battles, key=lambda k: k['rating'], reverse=True) pages = utils.Pages(ctx, entries=output)
await pages.paginate()
output = [] except utils.CannotPaginate as e:
for x in sorted_members: await ctx.send(str(e))
member_id = int(x['member_id'])
rating = x['rating']
member = ctx.message.guild.get_member(member_id)
output.append("{} (Rating: {})".format(member.display_name, rating))
try:
pages = utils.Pages(ctx, entries=output)
await pages.paginate()
except utils.CannotPaginate as e:
await ctx.send(str(e))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -282,8 +283,6 @@ class Stats:
EXAMPLE: !stats @OtherPerson EXAMPLE: !stats @OtherPerson
RESULT: How good they are at winning a completely luck based game""" RESULT: How good they are at winning a completely luck based game"""
await ctx.message.channel.trigger_typing()
member = member or ctx.message.author member = member or ctx.message.author
# Get the different data that we'll display # Get the different data that we'll display
server_rank = "{}/{}".format(*self.bot.br.get_server_rank(member)) server_rank = "{}/{}".format(*self.bot.br.get_server_rank(member))

View file

@ -20,8 +20,9 @@ class Tags:
EXAMPLE: !tags EXAMPLE: !tags
RESULT: All tags setup on this server""" RESULT: All tags setup on this server"""
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags') tags = await self.bot.db.fetch("SELECT trigger FROM tags WHERE guild=$1", ctx.guild.id)
if tags:
if len(tags) > 0:
entries = [t['trigger'] for t in tags] entries = [t['trigger'] for t in tags]
pages = utils.Pages(ctx, entries=entries) pages = utils.Pages(ctx, entries=entries)
await pages.paginate() await pages.paginate()
@ -36,16 +37,18 @@ class Tags:
EXAMPLE: !mytags EXAMPLE: !mytags
RESULT: All your tags setup on this server""" RESULT: All your tags setup on this server"""
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags') tags = await self.bot.db.fetch(
if tags: "SELECT trigger FROM tags WHERE guild=$1 AND creator=$2",
entries = [t['trigger'] for t in tags if t['author'] == str(ctx.message.author.id)] ctx.guild.id,
if len(entries) == 0: ctx.author.id
await ctx.send("You have no tags setup on this server!") )
else:
pages = utils.Pages(ctx, entries=entries) if len(tags) > 0:
await pages.paginate() entries = [t['trigger'] for t in tags]
pages = utils.Pages(ctx, entries=entries)
await pages.paginate()
else: else:
await ctx.send("There are no tags setup on this server!") await ctx.send("You have no tags on this server!")
@commands.group(invoke_without_command=True) @commands.group(invoke_without_command=True)
@commands.guild_only() @commands.guild_only()
@ -56,16 +59,17 @@ class Tags:
EXAMPLE: !tag butts EXAMPLE: !tag butts
RESULT: Whatever you setup for the butts tag!!""" RESULT: Whatever you setup for the butts tag!!"""
tag = tag.lower().strip() tag = await self.bot.db.fetchrow(
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags') "SELECT id, result FROM tags WHERE guild=$1 AND trigger=$2",
if tags: ctx.guild.id,
for t in tags: tag.lower().strip()
if t['trigger'].lower().strip() == tag: )
await ctx.send("\u200B{}".format(t['result']))
return if tag:
await ctx.send("There is no tag called {}".format(tag)) await ctx.send("\u200B{}".format(tag['result']))
await self.bot.db.execute("UPDATE tags SET uses = uses + 1 WHERE id = $1", tag['id'])
else: else:
await ctx.send("There are no tags setup on this server!") await ctx.send("There is no tag called {}".format(tag))
@tag.command(name='add', aliases=['create', 'setup']) @tag.command(name='add', aliases=['create', 'setup'])
@commands.guild_only() @commands.guild_only()
@ -88,22 +92,24 @@ class Tags:
return return
trigger = msg.content.lower().strip() trigger = msg.content.lower().strip()
forbidden_tags = ['add', 'create', 'setup', 'edit', ''] forbidden_tags = ['add', 'create', 'setup', 'edit', 'info', 'delete', 'remove', 'stop']
if len(trigger) > 100: if len(trigger) > 100:
await ctx.send("Please keep tag triggers under 100 characters") await ctx.send("Please keep tag triggers under 100 characters")
return return
elif trigger in forbidden_tags: elif trigger.lower() in forbidden_tags:
await ctx.send( await ctx.send(
"Sorry, but your tag trigger was detected to be forbidden. " "Sorry, but your tag trigger was detected to be forbidden. "
"Current forbidden tag triggers are: \n{}".format("\n".join(forbidden_tags))) "Current forbidden tag triggers are: \n{}".format("\n".join(forbidden_tags)))
return return
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags') or [] tag = await self.bot.db.fetchrow(
if tags: "SELECT result FROM tags WHERE guild=$1 AND trigger=$2",
for t in tags: ctx.guild.id,
if t['trigger'].lower().strip() == trigger: trigger.lower().strip()
await ctx.send("There is already a tag setup called {}!".format(trigger)) )
return if tag:
await ctx.send("There is already a tag setup called {}!".format(trigger))
return
try: try:
await my_msg.delete() await my_msg.delete()
@ -111,10 +117,6 @@ class Tags:
except (discord.Forbidden, discord.HTTPException): except (discord.Forbidden, discord.HTTPException):
pass pass
if trigger.lower() in ['edit', 'delete', 'remove', 'stop']:
await ctx.send("You can't create a tag with {}!".format(trigger))
return
my_msg = await ctx.send( my_msg = await ctx.send(
"Alright, your new tag can be called with {}!\n\nWhat do you want to be displayed with this tag?".format( "Alright, your new tag can be called with {}!\n\nWhat do you want to be displayed with this tag?".format(
trigger)) trigger))
@ -132,92 +134,97 @@ class Tags:
except (discord.Forbidden, discord.HTTPException): except (discord.Forbidden, discord.HTTPException):
pass pass
# The different DB settings
tag = {
'author': str(ctx.message.author.id),
'trigger': trigger,
'result': result
}
tags.append(tag)
entry = {
'server_id': str(ctx.message.guild.id),
'tags': tags
}
await self.bot.db.save('tags', entry)
await ctx.send("I have just setup a new tag for this server! You can call your tag with {}".format(trigger)) await ctx.send("I have just setup a new tag for this server! You can call your tag with {}".format(trigger))
await self.bot.db.execute(
"INSERT INTO tags(guild, creator, trigger, result) VALUES ($1, $2, $3, $4)",
ctx.guild.id,
ctx.author.id,
trigger,
result
)
@tag.command(name='edit') @tag.command(name='edit')
@commands.guild_only() @commands.guild_only()
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
async def edit_tag(self, ctx, *, tag: str): async def edit_tag(self, ctx, *, trigger: str):
"""This will allow you to edit a tag that you have created """This will allow you to edit a tag that you have created
EXAMPLE: !tag edit this tag EXAMPLE: !tag edit this tag
RESULT: I'll ask what you want the new result to be""" RESULT: I'll ask what you want the new result to be"""
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags')
def check(m): def check(m):
return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0 return m.channel == ctx.message.channel and m.author == ctx.message.author and len(m.content) > 0
if tags: tag = await self.bot.db.fetchrow(
for i, t in enumerate(tags): "SELECT id, trigger FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
if t['trigger'] == tag: ctx.guild.id,
if t['author'] == str(ctx.message.author.id): ctx.author.id,
my_msg = await ctx.send( trigger
"Alright, what do you want the new result for the tag {} to be".format(tag)) )
try:
msg = await self.bot.wait_for("message", check=check, timeout=60) if tag:
except asyncio.TimeoutError: my_msg = await ctx.send(f"Alright, what do you want the new result for the tag {tag} to be")
await ctx.send("You took too long!") try:
return msg = await self.bot.wait_for("message", check=check, timeout=60)
new_tag = t.copy() except asyncio.TimeoutError:
new_tag['result'] = msg.content await ctx.send("You took too long!")
tags[i] = new_tag return
try:
await my_msg.delete() new_result = msg.content
await msg.delete()
except discord.Forbidden: try:
pass await my_msg.delete()
entry = { await msg.delete()
'server_id': str(ctx.message.guild.id), except (discord.Forbidden, discord.HTTPException):
'tags': tags pass
}
await self.bot.db.save('tags', entry) await ctx.send(f"Alright, the tag {trigger} has been updated")
await ctx.send("Alright, the tag {} has been updated".format(tag)) await self.bot.db.execute("UPDATE tags SET result=$1 WHERE id=$2", new_result, tag['id'])
return
else:
await ctx.send("You can't edit someone else's tag!")
return
await ctx.send("There isn't a tag called {}!".format(tag))
else: else:
await ctx.send("There are no tags setup on this server!") await ctx.send(f"You do not have a tag called {trigger} on this server!")
@tag.command(name='delete', aliases=['remove', 'stop']) @tag.command(name='delete', aliases=['remove', 'stop'])
@commands.guild_only() @commands.guild_only()
@utils.can_run(send_messages=True) @utils.can_run(send_messages=True)
async def del_tag(self, ctx, *, tag: str): async def del_tag(self, ctx, *, trigger: str):
"""Use this to remove a tag from use for this server """Use this to remove a tag from use for this server
Format to delete a tag is !tag delete <tag> Format to delete a tag is !tag delete <tag>
EXAMPLE: !tag delete stupid_tag EXAMPLE: !tag delete stupid_tag
RESULT: Deletes that stupid tag""" RESULT: Deletes that stupid tag"""
tags = self.bot.db.load('tags', key=ctx.message.guild.id, pluck='tags')
if tags: tag = await self.bot.db.fetchrow(
for t in tags: "SELECT id FROM tags WHERE guild=$1 AND creator=$2 AND trigger=$3",
if t['trigger'].lower().strip() == tag: ctx.guild.id,
if ctx.message.author.permissions_in(ctx.message.channel).manage_guild or str( ctx.author.id,
ctx.message.author.id) == t['author']: trigger
tags.remove(t) )
entry = {
'server_id': str(ctx.message.guild.id), if tag:
'tags': tags await ctx.send(f"I have just deleted the tag {trigger}")
} await self.bot.db.execute("DELETE FROM tags WHERE id=$1", tag['id'])
await self.bot.db.save('tags', entry)
await ctx.send("I have just removed the tag {}".format(tag))
else:
await ctx.send("You don't own that tag! You can't remove it!")
return
else: else:
await ctx.send("There are no tags setup on this server!") await ctx.send(f"You do not own a tag called {trigger} on this server!")
@tag.command(name="info")
@commands.guild_only()
@utils.can_run(send_messages=True)
async def info_tag(self, ctx, *, trigger: str):
"""Shows some information a bout the tag given"""
tag = await self.bot.db.fetchrow(
"SELECT creator, uses, trigger FROM tags WHERE guild=$1 AND trigger=$3",
ctx.guild.id,
trigger
)
embed = discord.Embed(title=tag['trigger'])
creator = ctx.guild.get_member(tag['creator'])
if creator:
embed.set_author(name=creator.display_name, url=creator.avatar_url)
embed.add_field(name="Uses", value=tag['uses'])
embed.add_field(name="Owner", value=creator.mention)
await ctx.send(embed=embed)
def setup(bot): def setup(bot):

View file

@ -26,13 +26,17 @@ class Tutorial:
await ctx.send("Could not find a command or a cog for {}".format(cmd_or_cog)) await ctx.send("Could not find a command or a cog for {}".format(cmd_or_cog))
return return
commands = [c for c in utils.get_all_commands(self.bot) if c.cog_name == cmd_or_cog.title()] commands = set([
c
for c in self.bot.walk_commands()
if c.cog_name == cmd_or_cog.title()
])
# Specific command # Specific command
else: else:
commands = [cmd] commands = [cmd]
# Use all commands # Use all commands
else: else:
commands = list(utils.get_all_commands(self.bot)) commands = set(self.bot.walk_commands())
# Loop through all the commands that we want to use # Loop through all the commands that we want to use
for command in commands: for command in commands:

View file

@ -1,8 +1,8 @@
Pillow==4.2.0
rethinkdb rethinkdb
pyyaml pyyaml
psutil psutil
pendulum pendulum
beautifulsoup4 beautifulsoup4
osuapi osuapi
asyncpg
-e git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py -e git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py

View file

@ -1,6 +1,6 @@
from .cards import Deck, Face, Suit from .cards import Deck, Face, Suit
from .checks import can_run, db_check from .checks import can_run
from .config import * from .config import *
from .utilities import * from .utilities import *
from .paginator import Pages, CannotPaginate, HelpPaginator from .paginator import Pages, CannotPaginate, HelpPaginator
from .database import DB from .database import DB, Cache

View file

@ -8,69 +8,14 @@ from . import utilities
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
# The tables needed for the database, as well as their primary keys
required_tables = {
'battle_records': 'member_id',
'boops': 'member_id',
'command_usage': 'command',
'overwatch': 'member_id',
'picarto': 'member_id',
'server_settings': 'server_id',
'raffles': 'server_id',
'strawpolls': 'server_id',
'osu': 'member_id',
'tags': 'server_id',
'tictactoe': 'member_id',
'twitch': 'member_id',
'user_playlists': 'member_id',
'birthdays': 'member_id'
}
async def db_check():
"""Used to check if the required database/tables are setup"""
db_opts = config.db_opts
r.set_loop_type('asyncio')
# First try to connect, and see if the correct information was provided
try:
conn = await r.connect(**db_opts)
except r.errors.ReqlDriverError:
print("Cannot connect to the RethinkDB instance with the following information: {}".format(db_opts))
print("The RethinkDB instance you have setup may be down, otherwise please ensure you setup a"
" RethinkDB instance, and you have provided the correct database information in config.yml")
quit()
return
# Get the current databases and check if the one we need is there
dbs = await r.db_list().run(conn)
if db_opts['db'] not in dbs:
# If not, we want to create it
print('Couldn\'t find database {}...creating now'.format(db_opts['db']))
await r.db_create(db_opts['db']).run(conn)
# Then add all the tables
for table, key in required_tables.items():
print("Creating table {}...".format(table))
await r.table_create(table, primary_key=key).run(conn)
print("Done!")
else:
# Otherwise, if the database is setup, make sure all the required tables are there
tables = await r.table_list().run(conn)
for table, key in required_tables.items():
if table not in tables:
print("Creating table {}...".format(table))
await r.table_create(table, primary_key=key).run(conn)
print("Done checking tables!")
def should_ignore(ctx): def should_ignore(ctx):
if ctx.message.guild is None: if ctx.message.guild is None:
return False return False
ignored = ctx.bot.db.load('server_settings', key=ctx.message.guild.id, pluck='ignored') ignored = ctx.bot.cache.ignored[ctx.guild.id]
if not ignored: if not ignored:
return False return False
return str(ctx.message.author.id) in ignored['members'] or str(ctx.message.channel.id) in ignored['channels'] return ctx.message.author.id in ignored['members'] or ctx.message.channel.id in ignored['channels']
async def check_not_restricted(ctx): async def check_not_restricted(ctx):
@ -79,7 +24,7 @@ async def check_not_restricted(ctx):
return True return True
# First get all the restrictions # First get all the restrictions
restrictions = ctx.bot.db.load('server_settings', key=ctx.message.guild.id, pluck='restrictions') or {} restrictions = ctx.bot.cache.restrictions[ctx.guild.id]
# Now lets check the "from" restrictions # Now lets check the "from" restrictions
for from_restriction in restrictions.get('from', []): for from_restriction in restrictions.get('from', []):
# Get the source and destination # Get the source and destination
@ -169,8 +114,7 @@ def has_perms(ctx, **perms):
for perm, setting in perms.items(): for perm, setting in perms.items():
setattr(required_perm, perm, setting) setattr(required_perm, perm, setting)
required_perm_value = ctx.bot.db.load('server_settings', key=ctx.message.guild.id, pluck='permissions') or {} required_perm_value = ctx.bot.cache.custom_permissions[ctx.guild.id].get(ctx.command.qualified_name)
required_perm_value = required_perm_value.get(ctx.command.qualified_name)
if required_perm_value: if required_perm_value:
required_perm = discord.Permissions(required_perm_value) required_perm = discord.Permissions(required_perm_value)

View file

@ -57,6 +57,7 @@ extensions = [
'cogs.misc', 'cogs.misc',
'cogs.mod', 'cogs.mod',
'cogs.admin', 'cogs.admin',
'cogs.config',
'cogs.images', 'cogs.images',
'cogs.birthday', 'cogs.birthday',
'cogs.owner', 'cogs.owner',
@ -80,25 +81,21 @@ extensions = [
# The default status the bot will use # The default status the bot will use
default_status = global_config.get("default_status", None) default_status = global_config.get("default_status", None)
# The rethinkdb hostname # The database hostname
db_host = global_config.get('db_host', 'localhost') db_host = global_config.get('db_host', None)
# The rethinkdb database name # The database name
db_name = global_config.get('db_name', 'Discord_Bot') db_name = global_config.get('db_name', 'bonfire')
# The rethinkdb certification # The database port
db_cert = global_config.get('db_cert', '') db_port = global_config.get('db_port', None)
# The rethinkdb port
db_port = global_config.get('db_port', 28015)
# The user and password assigned # The user and password assigned
db_user = global_config.get('db_user', 'admin') db_user = global_config.get('db_user', None)
db_pass = global_config.get('db_pass', '') db_pass = global_config.get('db_pass', None)
# We've set all the options we need to be able to connect # We've set all the options we need to be able to connect
# so create a dictionary that we can use to unload to connect # so create a dictionary that we can use to unload to connect
# db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'ssl': db_opts = {'host': db_host, 'database': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
# {'ca_certs': db_cert}, 'user': db_user, 'password': db_pass}
db_opts = {'host': db_host, 'db': db_name, 'port': db_port, 'user': db_user, 'password': db_pass}
def command_prefix(bot, message): def command_prefix(bot, message):
if not message.guild: if not message.guild:
return default_prefix return default_prefix
return bot.db.load('server_settings', key=message.guild.id, pluck='prefix') or default_prefix return bot.cache.prefixes.get(message.guild.id, default_prefix)

View file

@ -1,61 +1,87 @@
import asyncio import asyncio
import rethinkdb as r import asyncpg
from datetime import datetime
from .checks import required_tables from collections import defaultdict
from . import config from . import config
async def _convert_to_list(cursor):
# This method is here because atm, AsyncioCursor is not iterable
# For our purposes, we want a list, so we need to do this manually
cursor_list = []
while True:
try:
val = await cursor.next()
cursor_list.append(val)
except r.ReqlCursorEmpty:
break
return cursor_list
class Cache: class Cache:
"""A class to hold the cached database entries""" """A class to hold the entires that are called on every message/command"""
def __init__(self, table, key, db, loop): def __init__(self, db):
self.table = table # The name of the database table self.db = db
self.key = key # The name of primary key self.prefixes = {}
self.db = db # The database class connections are made through self.ignored = defaultdict(dict)
self.loop = loop self.custom_permissions = defaultdict(dict)
self.values = {} # The values returned from the database self.restrictions = defaultdict(dict)
self.refreshed_time = None
self.loop.create_task(self.refresh_task())
async def refresh(self): async def setup(self):
self.values = await self.db.query(r.table(self.table).group(self.key)[0]) await self.load_prefixes()
self.refreshed_time = datetime.now() await self.load_custom_permissions()
await self.load_restrictions()
await self.load_ignored()
async def refresh_task(self): async def load_ignored(self):
await self.check_refresh() query = """
await asyncio.sleep(60) SELECT
id, ignored_channels, ignored_members
FROM
guilds
WHERE
array_length(ignored_channels, 1) > 0 OR
array_length(ignored_members, 1) > 0
"""
rows = await self.db.fetch(query)
for row in rows:
self.ignored[row['guild']]['members'] = row['ignored_members']
self.ignored[row['guild']]['channels'] = row['ignored_channels']
async def check_refresh(self): async def load_prefixes(self):
if self.refreshed_time is None: query = """
await self.refresh() SELECT
else: id, prefix
difference = datetime.now() - self.refreshed_time FROM
if difference.total_seconds() > 300: guilds
await self.refresh() WHERE
prefix IS NOT NULL
"""
rows = await self.db.fetch(query)
for row in rows:
self.prefixes[row['id']] = row['prefix']
def get(self, key=None, pluck=None): def update_prefix(self, guild, prefix):
"""This simulates the database call, to make it easier to get the data""" self.prefixes[guild.id] = prefix
value = self.values
if key:
value = value.get(str(key), {})
if pluck:
value = value.get(pluck)
return value async def load_custom_permissions(self):
query = """
SELECT
guild, command, permission
FROM
custom_permissions
WHERE
permission IS NOT NULL
"""
rows = await self.db.fetch(query)
for row in rows:
self.custom_permissions[row['guild']][row['command']] = row['permission']
def update_custom_permission(self, guild, command, permission):
self.custom_permissions[guild.id][command.qualified_name] = permission
async def load_restrictions(self):
query = """
SELECT
guild, source, from_to, destination
FROM
restrictions
"""
rows = await self.db.fetch(query)
for row in rows:
opt = {"source": row['source'], "destination": row['destination']}
from_restrictions = self.restrictions[row['guild']].get(row['from_to'], [])
from_restrictions.append(opt)
self.restrictions[row['guild']][row['from_to']] = from_restrictions
class DB: class DB:
@ -63,66 +89,40 @@ class DB:
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.opts = config.db_opts self.opts = config.db_opts
self.cache = {} self.cache = {}
self._pool = None
for table, key in required_tables.items(): async def connect(self):
self.cache[table] = Cache(table, key, self, self.loop) self._pool = await asyncpg.create_pool(**self.opts)
async def query(self, query): async def setup(self):
"""Lets you run a manual query""" await self.connect()
r.set_loop_type("asyncio")
conn = await r.connect(**self.opts)
try:
cursor = await query.run(conn)
except (r.ReqlOpFailedError, r.ReqlNonExistenceError):
cursor = None
if isinstance(cursor, r.Cursor):
cursor = await _convert_to_list(cursor)
await conn.close()
return cursor
# def save(self, table, content): async def _query(self, call, query, *args, **kwargs):
# """A synchronous task to throw saving content into a task""" """this will acquire a connection and make the call, then return the result"""
# self.loop.create_task(self._save(table, content)) async with self._pool.acquire() as connection:
async with connection.transaction():
return await getattr(connection, call)(query, *args, **kwargs)
async def save(self, table, content): async def execute(self, *args, **kwargs):
"""Saves data in the table""" return await self._query("execute", *args, **kwargs)
index = await self.query(r.table(table).info()) async def fetch(self, *args, **kwargs):
index = index.get("primary_key") return await self._query("fetch", *args, **kwargs)
key = content.get(index)
if key:
cur_content = await self.query(r.table(table).get(key))
if cur_content:
# We have content...we either need to update it, or replace
# Update will typically be more common so lets try that first
result = await self.query(r.table(table).get(key).update(content))
if result.get('replaced', 0) == 0 and result.get('unchanged', 0) == 0:
await self.query(r.table(table).get(key).replace(content))
else:
await self.query(r.table(table).insert(content))
else:
await self.query(r.table(table).insert(content))
await self.cache.get(table).refresh() async def fetchrow(self, *args, **kwargs):
return await self._query("fetchrow", *args, **kwargs)
def load(self, table, **kwargs): async def fetchval(self, *args, **kwargs):
return self.cache.get(table).get(**kwargs) return await self._query("fetchval", *args, **kwargs)
async def actual_load(self, table, key=None, table_filter=None, pluck=None): async def upsert(self, table, data):
"""Loads the specified content from the specific table""" keys = values = ""
query = r.table(table) for num, k in enumerate(data.keys()):
if num > 0:
# If a key has been provided, get content with that key keys += ", "
if key: values += ", "
query = query.get(str(key)) keys += k
# A key and a filter shouldn't be combined for any case we'll ever use, so seperate these values += f"${num}"
elif table_filter: query = f"INSERT INTO {table} ({keys}) VALUES ({values}) ON CONFLICT DO UPDATE"
query = query.filter(table_filter) print(query)
return await self.execute(query, *data.values())
# If we want to pluck something specific, do that
if pluck:
query = query.pluck(pluck).values()[0]
cursor = await self.query(query)
return cursor

View file

@ -5,47 +5,10 @@ import discord
from discord.ext import commands from discord.ext import commands
from . import config from . import config
from PIL import Image
def convert_to_jpeg(pfile): def channel_is_nsfw(channel):
# Open the file given return isinstance(channel, discord.DMChannel) or channel.is_nsfw()
img = Image.open(pfile)
# Create the BytesIO object we'll use as our new "file"
new_file = BytesIO()
# Save to this file as jpeg
img.save(new_file, format='JPEG')
# In order to use the file, we need to seek back to the 0th position
new_file.seek(0)
return new_file
def get_all_commands(bot):
"""Returns a list of all command names for the bot"""
# First lets create a set of all the parent names
for cmd in bot.commands:
yield from get_all_subcommands(cmd)
def get_all_subcommands(command):
yield command
if type(command) is discord.ext.commands.core.Group:
for subcmd in command.commands:
yield from get_all_subcommands(subcmd)
async def channel_is_nsfw(channel, db):
if type(channel) is discord.DMChannel:
server = 'DMs'
elif channel.is_nsfw():
return True
else:
server = str(channel.guild.id)
channel = str(channel.id)
channels = db.load('server_settings', key=server, pluck='nsfw_channels') or []
return channel in channels
async def download_image(url): async def download_image(url):
@ -103,8 +66,11 @@ async def request(url, *, headers=None, payload=None, method='GET', attr='json',
except: except:
continue continue
async def convert(ctx, option): async def convert(ctx, option):
"""Tries to convert a string to an object of useful representiation""" """Tries to convert a string to an object of useful representiation"""
# Due to id's being ints, it's very possible that an int is passed
option = str(option)
cmd = ctx.bot.get_command(option) cmd = ctx.bot.get_command(option)
if cmd: if cmd:
return cmd return cmd
@ -132,25 +98,52 @@ async def convert(ctx, option):
return role return role
def update_rating(winner_rating, loser_rating):
# The scale is based off of increments of 25, increasing the change by 1 for each increment
# That is all this loop does, increment the "change" for every increment of 25
# The change caps off at 300 however, so break once we are over that limit
difference = abs(winner_rating - loser_rating)
rating_change = 0
count = 25
while count <= difference:
if count > 300:
break
rating_change += 1
count += 25
# 16 is the base change, increased or decreased based on whoever has the higher current rating
if winner_rating > loser_rating:
winner_rating += 16 - rating_change
loser_rating -= 16 - rating_change
else:
winner_rating += 16 + rating_change
loser_rating -= 16 + rating_change
return winner_rating, loser_rating
async def update_records(key, db, winner, loser): async def update_records(key, db, winner, loser):
# We're using the Harkness scale to rate # We're using the Harkness scale to rate
# http://opnetchessclub.wikidot.com/harkness-rating-system # http://opnetchessclub.wikidot.com/harkness-rating-system
r_filter = lambda row: (row['member_id'] == str(winner.id)) | (row['member_id'] == str(loser.id)) wins = f"{key}_wins"
matches = await db.actual_load(key, table_filter=r_filter) losses = f"{key}_losses"
key = f"{key}_rating"
query = """
SELECT
id, $1, $2, $3
FROM
users
WHERE
id = any($4::bigint[])
"""
results = await db.fetch(key, wins, losses, [winner.id, loser.id])
winner_stats = {} winner_rating = loser_rating = 1000
loser_stats = {} for result in results:
try: if result['id'] == winner.id:
for stat in matches: winner_rating = result[key]
if stat.get('member_id') == str(winner.id): else:
winner_stats = stat loser_rating = result[key]
elif stat.get('member_id') == str(loser.id):
loser_stats = stat
except TypeError:
pass
winner_rating = winner_stats.get('rating') or 1000
loser_rating = loser_stats.get('rating') or 1000
# The scale is based off of increments of 25, increasing the change by 1 for each increment # The scale is based off of increments of 25, increasing the change by 1 for each increment
# That is all this loop does, increment the "change" for every increment of 25 # That is all this loop does, increment the "change" for every increment of 25