1
0
Fork 0
mirror of synced 2024-05-18 03:22:27 +12:00
Bonfire/cogs/raffle.py

302 lines
12 KiB
Python

from discord.ext import commands
import discord
import utils
import random
import pendulum
import re
import asyncio
import traceback
class Raffle:
"""Used to hold custom raffles"""
def __init__(self, bot):
self.bot = bot
self.bot.loop.create_task(self.raffle_task())
async def raffle_task(self):
while True:
try:
await self.check_raffles()
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):
# 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()
@utils.can_run(send_messages=True)
async def raffles(self, ctx):
"""Used to print the current running raffles on the server
EXAMPLE: !raffles
RESULT: A list of the raffles setup on this server"""
raffles = self.bot.db.load('raffles', key=ctx.message.guild.id, pluck='raffles')
if not raffles:
await ctx.send("There are currently no raffles setup on this server!")
return
# For EVERY OTHER COG, when we get one result, it is nice to have it return that exact object
# 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
if isinstance(raffles, dict):
raffles = [raffles]
fmt = "\n\n".join("**Raffle:** {}\n**Title:** {}\n**Total Entrants:** {}\n**Ends:** {} UTC".format(
num + 1,
raffle['title'],
len(raffle['entrants']),
raffle['expires']) for num, raffle in enumerate(raffles))
await ctx.send(fmt)
@commands.group(invoke_without_command=True)
@commands.guild_only()
@utils.can_run(send_messages=True)
async def raffle(self, ctx, raffle_id: int = 0):
"""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
EXAMPLE: !raffle 1
RESULT: You've entered the first raffle!"""
# Lets let people use 1 - (length of raffles) and handle 0 base ourselves
raffle_id -= 1
author = ctx.message.author
key = str(ctx.message.guild.id)
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:
fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \
"There are currently `{}` raffles running, use {}raffles to view the current running raffles".format(
raffle_count, ctx.prefix
)
await ctx.send(fmt)
@raffle.command(name='create', aliases=['start', 'begin', 'add'])
@commands.guild_only()
@utils.can_run(kick_members=True)
async def raffle_create(self, ctx):
"""This is used in order to create a new server raffle
EXAMPLE: !raffle create
RESULT: A follow-along for setting up a new raffle"""
author = ctx.message.author
server = ctx.message.guild
channel = ctx.message.channel
now = pendulum.now(tz="UTC")
await ctx.send(
"Ready to start a new raffle! Please respond with the title you would like to use for this raffle!")
check = lambda m: m.author == author and m.channel == channel
try:
msg = await self.bot.wait_for('message', check=check, timeout=120)
except asyncio.TimeoutError:
await ctx.send("You took too long! >:c")
return
title = msg.content
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 minimum for this is 10 minutes, and the maximum is 3 months"
await ctx.send(fmt.format(title))
# Our check to ensure that a proper length of time was passed
def check(m):
if m.author == author and m.channel == channel:
return re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", m.content.lower()) is not None
else:
return False
try:
msg = await self.bot.wait_for('message', timeout=120, check=check)
except asyncio.TimeoutError:
await ctx.send("You took too long! >:c")
return
# 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(' ')
# This should be safe to convert, we already made sure with our check earlier this would match
num = int(num)
# Now lets ensure this meets our min/max
if "minute" in term and (num < 10 or num > 129600):
await ctx.send(
"Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
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
# Pendulum only accepts the plural version of terms, lets make sure this is added
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!")
@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):
bot.add_cog(Raffle(bot))