commit f1138c3b56bb3c8ce7ebf69694f6096635792723 Author: Stijn De Clercq Date: Tue Oct 13 21:02:40 2020 +0200 Initial commit Transfer Didier to this repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2da7e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +files/status.txt +files/readyMessage.txt +files/client.txt +files/lastTasks.json +files/c4.json +files/hangman.json +files/stats.json +files/lost.json +files/locked.json +files/database.json +.idea/ +__pycache__ +.env \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6cbdc99 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.6.9 \ No newline at end of file diff --git a/cogs/birthdays.py b/cogs/birthdays.py new file mode 100644 index 0000000..2c3c284 --- /dev/null +++ b/cogs/birthdays.py @@ -0,0 +1,226 @@ +from data import constants +import datetime +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from functions import timeFormatters, stringFormatters +from functions.database import birthdays + + +class Birthdays(commands.Cog): + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + @commands.group(name="Birthday", aliases=["Bd", "Birthdays"], case_insensitive=True, invoke_without_command=True) + @help.Category(Category.Other) + async def birthday(self, ctx, member: discord.Member = None): + """ + Command to check the birthday of yourself/another person. + :param ctx: Discord Context + :param member: The member to check + """ + if member is not None: + # A member was tagged + nameStr = "**{}**'s".format(member.display_name) + res = birthdays.get_user(member.id) + else: + # No member passed -> check the user's birthday + nameStr = "Jouw" + res = birthdays.get_user(ctx.author.id) + + if not res: + # Nothing found in the db for this member + return await ctx.send("{} verjaardag zit nog niet in de database.".format(nameStr)) + + # Create a datetime object of the upcoming birthday, + # and a formatted string displaying the date + dayDatetime, timeString = self.dmToDatetime(res[0][0], res[0][1]) + + # Find the weekday related to this day + weekday = timeFormatters.intToWeekday(dayDatetime.weekday()).lower() + + return await ctx.send("{} verjaardag staat ingesteld op **{} {}**.".format( + nameStr, weekday, timeString + )) + + @birthday.command(name="Today", aliases=["Now"]) + async def today(self, ctx): + """ + Command that lists all birthdays of the day. + :param ctx: Discord Context + """ + # Create a datetime object for today + dt = timeFormatters.dateTimeNow() + await ctx.send(self.getBirthdayOnDate(dt)) + + @birthday.command(name="Tomorrow", aliases=["Tm", "Tmw"]) + async def tomorrow(self, ctx): + """ + Command that lists all birthdays of tomorrow. + :param ctx: Discord Context + """ + # Create a datetime object for tomorrow + dt = timeFormatters.dateTimeNow() + datetime.timedelta(days=1) + await ctx.send(self.getBirthdayOnDate(dt).replace("Vandaag", "Morgen").replace("vandaag", "morgen")) + + @birthday.command(name="Week") + async def week(self, ctx): + """ + Command that lists all birthdays for the coming week. + :param ctx: Discord Context + """ + # Dict of all birthdays this week + this_week = {} + + # Create a datetime object starting yesterday so the first line + # of the loop can add a day every time, + # as premature returning would prevent this from happening + # & get the day stuck + dt = timeFormatters.dateTimeNow() - datetime.timedelta(days=1) + + # Create an embed + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="Verjaardagen deze week") + + # Add all people of the coming week + for dayCounter in range(7): + dt += datetime.timedelta(days=1) + res = birthdays.get_users_on_date(dt.day, dt.month) + + # No birthdays on this day + if not res: + continue + + # Add everyone from this day into the dict + this_week[str(dayCounter)] = {"day": dt.day, "month": dt.month, "users": []} + + for user in res: + this_week[str(dayCounter)]["users"].append(user[0]) + + # No one found + if not this_week: + embed.description = "Deze week is er niemand jarig." + return await ctx.send(embed=embed) + + COC = self.client.get_guild(int(constants.CallOfCode)) + + # For every day, add the list of users into the embed + for day, value in this_week.items(): + + dayDatetime, timeString = self.dmToDatetime(int(value["day"]), int(value["month"])) + weekday = timeFormatters.intToWeekday(dayDatetime.weekday()) + + embed.add_field(name="{} {}".format(weekday, timeString), + value=", ".join(COC.get_member(user).mention for user in value["users"]), + inline=False) + + await ctx.send(embed=embed) + + def getBirthdayOnDate(self, dt): + """ + Function to get all birthdays on a certain date. + Returns a string right away to avoid more code duplication. + :param dt: the date (Python datetime instance) + :return: A formatted string containing all birthdays on [dt] + """ + res = birthdays.get_users_on_date(dt.day, dt.month) + + # Nobody's birthday + if not res: + return "Vandaag is er niemand jarig." + + COC = self.client.get_guild(int(constants.CallOfCode)) + + # Create a list of member objects of the people that have a birthday on this date + people = [COC.get_member(int(user[0])) for user in res] + + if len(people) == 1: + return "Vandaag is **{}** jarig.".format(people[0].display_name) + return "Vandaag zijn {} en {} jarig.".format( + ", ".join("**" + user.display_name + "**" for user in people[:-1]), + people[-1].display_name + ) + + def dmToDatetime(self, day, month): + """ + Converts a day + month to a datetime instance. + :param day: the day in the date + :param month: the month in the date + :return: a datetime instance representing the next time this date occurs, + and a formatted string for this date + """ + now = timeFormatters.dateTimeNow() + year = now.year + + # Add an extra year to the date in case it has already passed + if month < now.month or (month == now.month and day < now.day): + year += 1 + + # Create a datetime object for this birthday + timeString = "{}/{}/{}".format( + stringFormatters.leadingZero(str(day)), + stringFormatters.leadingZero(str(month)), + year + ) + + dayDatetime = datetime.datetime.strptime(timeString, "%d/%m/%Y") + return dayDatetime, timeString + + @birthday.command(name="Set", usage="[DD/MM/YYYY]") + async def set(self, ctx, date=None, member: discord.Member = None): + """ + Command to add your birthday into the database. + :param ctx: Discord Context + :param date: the date of your birthday + :param member: another member whose birthday has to be added/changed + """ + # No date passed + if date is None: + return await ctx.send("Geef een datum op.") + + # Invalid format used + if date.count("/") != 2: + return await ctx.send("Ongeldig formaat (gebruik DD/MM/YYYY).") + + # Check if anything is wrong with the date + try: + day = int(date.split("/")[0]) + month = int(date.split("/")[1]) + year = int(date.split("/")[2]) + + # This is not used, but creating an invalid datetime object throws a ValueError + # so it prevents invalid dates like 69/420/360 + dt = datetime.datetime(year=year, month=month, day=day) + + # Assume no one in the Discord is more than 5 years younger, or 10 years older + # (which are also virtually impossible, but just to be sure) + if year >= timeFormatters.dateTimeNow().year - 15 or year < 1990: + raise ValueError + + except ValueError: + return await ctx.send("Dit is geen geldige datum.") + + # A member was tagged, check if I did it + if member is not None: + if str(ctx.author.id) != str(constants.myId): + return await ctx.send("Je kan andere mensen hun verjaardag niet instellen, {}.".format(ctx.author.display_name)) + else: + birthdays.add_user(member.id, day, month, year) + return await ctx.message.add_reaction("✅") + + # Birthday is already added + if birthdays.get_user(ctx.author.id) and str(ctx.author.id) != constants.myId: + return await ctx.send("Je verjaardag zit al in de database.") + + # Add into the db + birthdays.add_user(ctx.author.id, day, month, year) + return await ctx.send("Je verjaardag is toegevoegd aan de database.") + + +def setup(client): + client.add_cog(Birthdays(client)) diff --git a/cogs/bitcoin.py b/cogs/bitcoin.py new file mode 100644 index 0000000..14a171a --- /dev/null +++ b/cogs/bitcoin.py @@ -0,0 +1,149 @@ +from converters.numbers import Abbreviated +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from functions import checks, timeFormatters +from functions.database import currency +import requests + + +class Bitcoin(commands.Cog): + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + @commands.group(name="Bitcoin", aliases=["Bc"], case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def bc(self, ctx): + """ + Command that shows your Bitcoin bank. + :param ctx: Discord Context + """ + price = self.getPrice() + bc = float(currency.getOrAddUser(ctx.author.id)[8]) + + currentTime = timeFormatters.dateTimeNow() + currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S') + + # Create the embed + embed = discord.Embed(colour=discord.Colour.gold()) + embed.set_author(name="Bitcoin Bank van {}".format(ctx.author.display_name)) + embed.add_field(name="Aantal Bitcoins:", value="{:,}".format(round(bc, 8)), inline=False) + embed.add_field(name="Huidige waarde:", value="{:,} Didier Dink{}" + .format(round(bc * price, 8), checks.pluralS(bc * price)), inline=False) + embed.set_footer(text="Huidige Bitcoin prijs: €{:,} ({})".format(price, str(currentTimeFormatted))) + + # Add the Bitcoin icon to the embed + file = discord.File("files/images/bitcoin.png", filename="icon.png") + embed.set_thumbnail(url="attachment://icon.png") + + await ctx.send(embed=embed, file=file) + + @bc.command(name="Price") + async def price(self, ctx): + """ + Command that shows the current Bitcoin price. + :param ctx: Discord Context + """ + price = self.getPrice() + currentTime = timeFormatters.dateTimeNow() + currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S') + await ctx.send( + "Huidige Bitcoin prijs: **€{:,}** ({}).".format(price, str(currentTimeFormatted))) + + @bc.command(name="Buy", usage="[Aantal]") + async def buy(self, ctx, amount: Abbreviated): + """ + Command to buy Bitcoins. + :param ctx: Discord Context + :param amount: the amount of Bitcoins the user wants to buy + """ + + resp = checks.isValidAmount(ctx, amount) + + # Not a valid amount: send the appropriate error message + if not resp[0]: + return await ctx.send(resp[1]) + + if amount == "all": + amount = resp[1] + + # Calculate the amount of Bitcoins the user can buy with [amount] of Didier Dinks + price = self.getPrice() + purchased = round(float(amount) / price, 8) + + # Update the db + currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - float(amount)) + currency.update(ctx.author.id, "bitcoins", + float(currency.getOrAddUser(ctx.author.id)[8]) + float(purchased)) + + await ctx.send("**{}** heeft **{:,}** Bitcoin{} gekocht voor **{:,}** Didier Dink{}!" + .format(ctx.author.display_name, purchased, checks.pluralS(purchased), + round(float(amount)), checks.pluralS(amount))) + + @bc.command(name="Sell", usage="[Aantal]") + async def sell(self, ctx, amount: Abbreviated): + """ + Command to sell Bitcoins. + :param ctx: Discord Context + :param amount: the amount of Bitcoins the user wants to sell + """ + if amount == "all": + amount = float(currency.getOrAddUser(ctx.author.id)[8]) + + try: + amount = float(amount) + if amount <= 0: + raise ValueError + + bc = float(currency.getOrAddUser(ctx.author.id)[8]) + + if bc == 0.0: + # User has no Bitcoins + await ctx.send("Je hebt geen Bitcoins, **{}**".format(ctx.author.display_name)) + elif amount > bc: + # User is trying to sell more Bitcoins that he has + await ctx.send("Je hebt niet genoeg Bitcoins om dit te doen, **{}**" + .format(ctx.author.display_name)) + else: + price = self.getPrice() + dinks = float(currency.dinks(ctx.author.id)) + + currency.update(ctx.author.id, "bitcoins", bc - amount) + currency.update(ctx.author.id, "dinks", dinks + (price * amount)) + + await ctx.send("**{}** heeft **{:,}** Bitcoin{} verkocht voor **{:,}** Didier Dink{}!" + .format(ctx.author.display_name, round(amount, 8), checks.pluralS(amount), + round((price * amount), 8), checks.pluralS(price * amount))) + except ValueError: + # Can't be parsed to float -> random string OR smaller than 0 + await ctx.send("Geef een geldig bedrag op.") + + @bc.command(aliases=["Lb", "Leaderboards"], hidden=True) + @help.Category(category=Category.Other) + async def leaderboard(self, ctx): + """ + Command that shows the Bitcoin Leaderboard. + Alias for Lb Bc. + :param ctx: Discord Context + """ + # Call the appropriate leaderboard function + await self.client.get_cog("Leaderboards").callLeaderboard("bitcoin", ctx) + + def getPrice(self): + """ + Function to get the current Bitcoin price. + :return: the current Bitcoin price (float) + """ + result = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json").json() + currentPrice = result["bpi"]["EUR"]["rate_float"] + return float(currentPrice) + + +def setup(client): + client.add_cog(Bitcoin(client)) diff --git a/cogs/corona.py b/cogs/corona.py new file mode 100644 index 0000000..64c4d4c --- /dev/null +++ b/cogs/corona.py @@ -0,0 +1,296 @@ +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from functions import checks, timeFormatters +import requests + + +class Corona(commands.Cog): + + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + # Gets the information & calls other functions if necessary + @commands.group(name="Corona", usage="[Land]*", case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Other) + async def corona(self, ctx, country: str = "Belgium"): + """ + Command that shows the corona stats for a certain country. + :param ctx: Discord Context + :param country: the country to show the stats for + """ + dic = await self.getCountryStats(country) + if dic is None: + # Country was not found + await self.sendError(ctx) + return + + await self.sendEmbed(ctx, dic) + + @corona.command(aliases=["lb", "leaderboards"], hidden=True) + async def leaderboard(self, ctx): + """ + Command that shows the Corona Leaderboard. + Alias for Lb Corona. + :param ctx: Discord Context + :return: y + """ + await self.client.get_cog("Leaderboards").callLeaderboard("corona", ctx) + + async def sendEmbed(self, ctx, dic): + """ + Function that sends a Corona embed from a dictionary. + :param ctx: Discord Context + :param dic: the dictionary corresponding to this country + """ + embed = discord.Embed(colour=discord.Colour.red(), title="Coronatracker {}".format(dic["today"]["country"])) + embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png") + + # Total + embed.add_field(name="Totale Gevallen (Vandaag):", + value="{:,} **(+{:,})** {}".format( + dic["today"]["cases"], + dic["today"]["todayCases"], + self.trendIndicator(dic, "todayCases") + ), + inline=False) + + # Active + embed.add_field(name="Actieve Gevallen (Vandaag):", + value="{:,} **(+{:,})** {}".format( + dic["today"]["activeCases"], + dic["today"]["activeCases"] - dic["yesterday"]["activeCases"], + self.activeTrendIndicator(dic) + ), + inline=False) + + # Deaths + embed.add_field(name="Sterfgevallen (Vandaag):", + value="{:,} **(+{:,})** {}".format( + dic["today"]["deaths"], + dic["today"]["todayDeaths"], + self.trendIndicator(dic, "todayDeaths") + ), + inline=False) + + # Recovered + embed.add_field(name="Hersteld (Vandaag):", + value="{:,} **(+{:,}) {}**".format( + dic["today"]["recovered"], + dic["today"]["todayRecovered"], + self.trendIndicator(dic, "todayRecovered") + ), + inline=False) + + # Test Cases + embed.add_field(name="Aantal uitgevoerde tests:", + value="{:,}".format(dic["today"]["tests"]), + inline=False) + + # Timestamp of last update + timeFormatted = timeFormatters.epochToDate(dic["today"]["updated"]) + embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format( + timeFormatted["date"], timeFormatted["timeAgo"])) + await ctx.send(embed=embed) + + @commands.command(name="Trends", aliases=["Ct"], usage="[Land]*") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Other) + async def trends(self, ctx, country: str = "Belgium"): + """ + Command that gives more precise stats & changes. + :param ctx: Discord Context + :param country: the country to get the stats for + """ + dic = await self.getCountryStats(country) + if dic is None: + await self.sendError(ctx) + return + + # Get the distribution for this country + distribution = self.distribution(dic) + + embed = discord.Embed(colour=discord.Colour.red(), title="Coronatrends {}".format(dic["today"]["country"])) + embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png") + + # Calculate the trends & add them into the fields + embed.add_field(name="Totale Gevallen\n({:,})".format(dic["today"]["cases"]), + value=self.trend(dic, "cases"), + inline=True) + + embed.add_field(name="Sterfgevallen\n({:,})".format(dic["today"]["deaths"]), + value=self.trend(dic, "deaths"), + inline=True) + + embed.add_field(name="Hersteld\n({:,})".format(dic["today"]["recovered"]), + value=self.trend(dic, "recovered")) + + embed.add_field(name="Totale Gevallen\nVandaag ({:,})".format(dic["today"]["todayCases"]), + value=self.trend(dic, "todayCases"), + inline=True) + + embed.add_field(name="Sterfgevallen\nVandaag ({:,})".format(dic["today"]["todayDeaths"]), + value=self.trend(dic, "todayDeaths"), + inline=True) + + embed.add_field(name="Hersteld\nVandaag ({:,})".format(dic["today"]["todayRecovered"]), + value=self.trend(dic, "todayRecovered")) + + embed.add_field(name="Verdeling", value="Actief: {} | Overleden: {} | Hersteld: {}".format( + distribution[0], distribution[1], distribution[2]), inline=False) + + # Timestamp of last update + timeFormatted = timeFormatters.epochToDate(dic["today"]["updated"]) + embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format( + timeFormatted["date"], timeFormatted["timeAgo"])) + await ctx.send(embed=embed) + + async def getCountryStats(self, country): + """ + Function that gets the stats for a specific country. + :param country: the country to get the stats for + :return: a dictionary containing the info for today & yesterday + """ + # Check if Global or a country was passed + if country.lower() == "global": + country = "all?" + else: + country = "countries/{}?strict=false&".format(country) + + today = requests.get("https://disease.sh/v3/covid-19/{}yesterday=false&allowNull=false".format(country)).json() + + # Send error message + if "message" in today: + return None + + yesterday = requests.get("https://disease.sh/v3/covid-19/{}yesterday=true&allowNull=false".format(country)) \ + .json() + + # Divide into today & yesterday to be able to calculate the changes + dic = { + "today": { + "country": today["country"] if country != "all?" else "Global", + "cases": today["cases"], + "activeCases": today["active"], + "todayCases": today["todayCases"], + "deaths": today["deaths"], + "todayDeaths": today["todayDeaths"], + "recovered": today["recovered"], + "todayRecovered": today["todayRecovered"], + "tests": today["tests"], + "updated": today["updated"] + }, + "yesterday": { + "cases": yesterday["cases"], + "activeCases": yesterday["active"], + "todayCases": yesterday["todayCases"], + "deaths": yesterday["deaths"], + "todayDeaths": yesterday["todayDeaths"], + "recovered": yesterday["recovered"], + "todayRecovered": yesterday["todayRecovered"], + "tests": yesterday["tests"], + "updated": yesterday["updated"] + } + } + return dic + + def distribution(self, dic): + """ + Calculates the percentage distribution for every key & shows an indicator. + :param dic: the today/yesterday dictionary for this country + :return: a list containing the distribution + indicator for active, recovered & deaths + """ + totalToday = dic["today"]["cases"] if dic["today"]["cases"] != 0 else 1 + totalYesterday = dic["yesterday"]["cases"] if dic["yesterday"]["cases"] != 0 else 1 + + tap = round(100 * dic["today"]["activeCases"]/totalToday, 2) # Today Active Percentage + trp = round(100 * dic["today"]["recovered"]/totalToday, 2) # Today Recovered Percentage + tdp = round(100 * dic["today"]["deaths"]/totalToday, 2) # Today Deaths Percentage + yap = round(100 * dic["yesterday"]["activeCases"] / totalYesterday, 2) # Yesterday Active Percentage + yrp = round(100 * dic["yesterday"]["recovered"] / totalYesterday, 2) # Yesterday Recovered Percentage + ydp = round(100 * dic["yesterday"]["deaths"] / totalYesterday, 2) # Yesterday Deaths Percentage + + return ["{}% {}".format(tap, self.indicator(tap, yap)), + "{}% {}".format(tdp, self.indicator(tdp, ydp)), + "{}% {}".format(trp, self.indicator(trp, yrp))] + + async def sendError(self, ctx): + """ + Function that sends an error embed when an invalid country was passed. + :param ctx: Discord Context + """ + embed = discord.Embed(colour=discord.Colour.red()) + embed.add_field(name="Error", value="Dit land staat niet in de database.", inline=False) + await ctx.send(embed=embed) + + # Returns a number and a percentage of rise/decline + def trend(self, dic, key): + """ + Function that creates a string representing a number & percentage of + rise & decline for a certain key of the dict. + :param dic: the today/yesterday dictionary for this country + :param key: the key to compare + :return: a string showing the increase in numbers & percentages + """ + # Difference vs yesterday + change = dic["today"][key] - dic["yesterday"][key] + + # Don't divide by 0 + yesterday = dic["yesterday"][key] if dic["yesterday"][key] != 0 else 1 + + # Percentage + perc = round(100 * change/yesterday, 2) + + # Sign to add to the number + sign = "+" if change >= 0 else "" + + return "{}{:,} ({}{:,}%)".format(sign, change, sign, perc) + + # Requires a bit of math so this is a separate function + def activeTrendIndicator(self, dic): + """ + Function that returns a rise/decline indicator for the active cases of the day. + This is a separate function as it requires some math to get right. + New cases have to take into account the deaths & recovered cases being + subtracted as well. + :param dic: the today/yesterday dictionary for this country + :return: a triangle emoji or empty string + """ + todayNew = dic["today"]["todayCases"] - dic["today"]["todayDeaths"] - dic["today"]["todayRecovered"] + yesterdayNew = dic["yesterday"]["todayCases"] - dic["yesterday"]["todayDeaths"] - dic["yesterday"]["todayRecovered"] + + return ":small_red_triangle:" if todayNew > yesterdayNew else \ + (":small_red_triangle_down:" if todayNew < yesterdayNew else "") + + # Returns an arrow indicating rise or decline + def trendIndicator(self, dic, key): + """ + Function that returns a rise/decline indicator for the target key. + :param dic: the today/yesterday dictionary for this country + :param key: the key to get the indicator for + :return: a triangle emoji or empty string + """ + return ":small_red_triangle:" if dic["today"][key] > dic["yesterday"][key] else \ + (":small_red_triangle_down:" if dic["today"][key] < dic["yesterday"][key] else "") + + # Also returns an indicator, but compares instead of pulling it out of the dic (for custom numbers) + def indicator(self, today, yesterday): + """ + Function that also returns an indicator but for two numbers + instead of comparing values out of the dictionary. + :param today: the number representing today + :param yesterday: the number representing yesterday + :return: a triangle emoji or empty string + """ + return ":small_red_triangle:" if today > yesterday else \ + (":small_red_triangle_down:" if yesterday > today else "") + + +def setup(client): + client.add_cog(Corona(client)) diff --git a/cogs/define.py b/cogs/define.py new file mode 100644 index 0000000..1adc440 --- /dev/null +++ b/cogs/define.py @@ -0,0 +1,113 @@ +import os + +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from functions import checks +import requests + + +class Define(commands.Cog): + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + @commands.command(name="Define", aliases=["UrbanDictionary", "Ud"], usage="[Woord]") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Other) + async def define(self, ctx, *words): + """ + Command that looks up the definition of a word in the Urban Dictionary. + :param ctx: Discord Context + :param words: Word(s) to look up + """ + words = list(words) + if len(words) == 0: + return await ctx.send("Controleer je argumenten.") + + query = " ".join(words) + answer = self.lookup(query) + + embed = discord.Embed(colour=discord.Colour.from_rgb(220, 255, 0)) + embed.set_author(name="Urban Dictionary") + + embed.add_field(name="Woord", value=answer["word"], inline=True) + embed.add_field(name="Auteur", value=answer["author"], inline=True) + embed.add_field(name="Definitie", value=self.cleanString(answer["definition"]), inline=False) + embed.add_field(name="Voorbeeld", value=self.cleanString(answer["example"]), inline=False) + embed.add_field(name="Rating", value=str(round(self.ratio(answer), 2)) + "%") + embed.add_field(name="Link naar de volledige definitie", + value="[Urban Dictionary]({})".format(str(answer["link"]))) + + await ctx.send(embed=embed) + + def lookup(self, word): + """ + Function that sends the API request to get the definition. + :param word: the woord to look up + :return: a dictionary representing the info of this word + """ + url = "https://mashape-community-urban-dictionary.p.rapidapi.com/define" + + querystring = {"term": word} + + headers = { + 'x-rapidapi-host': "mashape-community-urban-dictionary.p.rapidapi.com", + 'x-rapidapi-key': os.getenv("URBANDICTIONARY") + } + + try: + if word.lower() == "didier": + raise Exception + + response = requests.request("GET", url, headers=headers, params=querystring).json()["list"] + + if len(response) > 0: + return {"word": response[0]["word"], "definition": response[0]["definition"], + "example": response[0]["example"], "thumbs_up": response[0]["thumbs_up"], + "thumbs_down": response[0]["thumbs_down"], "link": response[0]["permalink"], + "author": response[0]["author"]} + + # No valid response + return self.defineDidier() + except Exception: + return self.defineDidier() + + def cleanString(self, text: str): + """ + Function that cuts off definitions that are too long & strips out UD markdown + from an input string. + :param text: the input string to clean up + :return: the edited version of the string + """ + text = text.replace("[", "") + text = text.replace("]", "") + return text if len(text) < 1024 else text[:1021] + "..." + + def ratio(self, dic): + """ + Function that alculates the upvote/downvote ratio of the definition. + :param dic: the dictionary representing the definition + :return: the upvote/downvote ratio (float) + """ + return (100 * int(dic["thumbs_up"])) / (int(dic["thumbs_up"]) + int(dic["thumbs_down"])) \ + if int(dic["thumbs_down"]) != 0 else 100.0 + + def defineDidier(self): + """ + Function that returns a stock dictionary to define Didier + in case people call it, or no definition was found. + :return: a dictionary that defines Didier + """ + return {"word": "Didier", "definition": "Didier", "example": "1: Didier\n2: Hmm?", "thumbs_up": 69420, + "thumbs_down": 0, "author": "Didier", + "link": "https://upload.wikimedia.org/wikipedia/commons/a/a5" + "/Didier_Reynders_in_Iranian_Parliament_02.jpg"} + + +def setup(client): + client.add_cog(Define(client)) diff --git a/cogs/dinks.py b/cogs/dinks.py new file mode 100644 index 0000000..25281d6 --- /dev/null +++ b/cogs/dinks.py @@ -0,0 +1,567 @@ +from converters.numbers import Abbreviated, abbreviated +from data import constants +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from enums.numbers import Numbers +from functions import checks +from functions.database import currency, prison, stats +from functions.numbers import getRep +import json +import math +import random + + +def calcCapacity(level): + """ + Function that calculates the rob capacity for a given level. + :param level: the level of the user + :return: the capacity the user can rob (float) + """ + cap = 200 + for x in range(level): + cap *= (math.pow(1.03, x)) + return round(cap) + + +class Dinks(commands.Cog): + + def __init__(self, client): + self.client = client + self.utilsCog = self.client.get_cog("Utils") + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + @commands.command(name="Award", aliases=["Reward"], usage="[@Persoon] [Aantal]", hidden=True) + @commands.check(checks.isMe) + @help.Category(category=Category.Mod) + async def award(self, ctx, user: discord.User, amount: Abbreviated): + """ + Command that awards a user a certain amount of Didier Dinks. + :param ctx: Discord Context + :param user: the user to give the Didier Dinks to + :param amount: the amount of Didier Dinks to award [user] + """ + # No amount was passed + if amount is None: + return + + # Update the db + currency.update(user.id, "dinks", float(currency.dinks(user.id)) + float(amount)) + + # Gets the abbreviated representation of the amount + rep = getRep(amount, Numbers.t.value) + + await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} beloond!" + .format(ctx.author.display_name, self.utilsCog.getDisplayName(ctx, user.id), rep, checks.pluralS(amount))) + + @commands.group(name="Dinks", aliases=["Cash"], case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def dinks(self, ctx): + """ + Command that shows the user's Didier Dinks & Platinum Dinks + :param ctx: Discord Context + """ + dinks = currency.dinksAll(ctx.author.id) + + answer = "**{}** heeft **{:,}** Didier Dink{}"\ + .format(ctx.author.display_name, math.floor(dinks["dinks"]), checks.pluralS(dinks["dinks"])) + + if dinks["platinum"] > 0: + answer += " en **{}** Platinum Dink{}".format(dinks["platinum"], checks.pluralS(dinks["platinum"])) + + await ctx.send(answer + "!") + + @dinks.command(aliases=["Lb", "Leaderboards"], hidden=True) + @commands.check(checks.allowedChannels) + async def leaderboard(self, ctx): + """ + Command that shows the Didier Dinks Leaderboard. + Alias for Lb Dinks. + :param ctx: Discord Context + """ + await self.client.get_cog("Leaderboards").callLeaderboard("dinks", ctx) + + @commands.command(name="Nightly") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def nightly(self, ctx): + """ + Command to claim daily Didier Dinks. + :param ctx: Discord Context + """ + response = currency.nightly(int(ctx.author.id)) + if response[0]: + # Claim successful + await ctx.send("Je hebt je dagelijkse **{:,}** Didier Dinks geclaimt. :fire:**{}**".format( + response[1], response[2])) + else: + # Already claimed today, react PIPO + await ctx.send("Je kan dit niet meerdere keren per dag doen.") + reactCog = self.client.get_cog("ReactWord") + await reactCog.react(ctx, "pipo") + + @commands.command(name="Give", aliases=["Gift"], usage="[@Persoon] [Aantal]") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def give(self, ctx, person: discord.Member, amount: Abbreviated): + """ + Command that gives your Didier Dinks to another user. + :param ctx: Discord Context + :param person: user to give the Didier Dinks to + :param amount: the amount of Didier Dinks to give + """ + # Invalid amount + if amount is None: + return + + valid = checks.isValidAmount(ctx, amount) + if not valid[0]: + return await ctx.send(valid[1]) + + authorDinks = float(currency.dinks(ctx.author.id)) + if amount == "all": + amount = authorDinks + + amount = float(amount) + + currency.update(person.id, "dinks", float(currency.dinks(person.id)) + amount) + currency.update(ctx.author.id, "dinks", authorDinks - amount) + + rep = getRep(math.floor(amount), Numbers.t.value) + + await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} geschonken!" + .format(ctx.author.display_name, person.display_name, + rep, checks.pluralS(amount))) + + @commands.group(name="Bank", aliases=["B"], case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def bank(self, ctx): + """ + Command that shows the user's Didier Bank. + :param ctx: Discord Context + """ + # 0 1 2 3 4 5 6 7 8 9 10 + # ID dinks level investedamount investeddays profit defense offense bc nightly streak + response = currency.getOrAddUser(ctx.author.id) + + # Calculate the cost to level your bank + interestLevelPrice = round(math.pow(1.28, int(response[2])) * 300) + ratio = round(float(1 * (1 + (int(response[2]) * 0.01))), 4) + + # Calculate the amount of levels the user can purchase + counter = 0 + sumPrice = float(math.pow(1.28, int(response[2])) * 300) + while float(response[1]) + float(response[3]) + float(response[5]) > sumPrice: + counter += 1 + sumPrice += round(float(math.pow(1.28, int(response[2]) + counter) * 300), 4) + maxLevels = "" if counter == 0 else " (+{})".format(str(counter)) + + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="Bank van {}".format(ctx.author.display_name)) + embed.set_thumbnail(url=str(ctx.author.avatar_url)) + embed.add_field(name="Level:", value=str(response[2]) + maxLevels, inline=True) + embed.add_field(name="Ratio:", value=str(ratio), inline=True) + embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(interestLevelPrice), inline=False) + embed.add_field(name="Momenteel geïnvesteerd:", value="{:,}".format(math.floor(float(response[3]))), inline=False) + embed.add_field(name="Aantal dagen geïnvesteerd:", value=str(response[4]), inline=True) + embed.add_field(name="Huidige winst na claim:", value="{:,}".format(math.floor(response[5])), inline=False) + await ctx.send(embed=embed) + + @bank.command(name="Stats") + async def stats(self, ctx): + """ + Command that shows the user's bank stats. + :param ctx: Discord Context + """ + response = currency.getOrAddUser(ctx.author.id) + + # Calculate the prices to level stats up + defense = int(response[6]) + defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6) + offense = int(response[7]) + capacity = calcCapacity(offense) + offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity + + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="Bank van {}".format(ctx.author.display_name)) + embed.add_field(name="Offense:", value=str(offense), inline=True) + embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(offenseLevelPrice)), inline=True) + embed.add_field(name="Capaciteit:", value="{:,}".format(int(capacity)), inline=True) + embed.add_field(name="Security:", value=str(defense), inline=True) + embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(defenseLevelPrice)), inline=True) + await ctx.send(embed=embed) + + @bank.group(name="Upgrade", aliases=["U"], case_insensitive=True, usage="[Categorie]", invoke_without_command=True) + async def upgrade(self, ctx): + """ + Command group to upgrade bank stats, + calling the group itself does nothing. + :param ctx: Discord Context + """ + pass + + @upgrade.command(name="Level", aliases=["L"], hidden=True) + async def level(self, ctx): + """ + Command that upgrades the user's bank level, + increasing interest. + :param ctx: Discord Context + """ + response = currency.getOrAddUser(ctx.author.id) + interestLevelPrice = float(math.pow(1.28, int(response[2])) * 300) + + # Check if user has enough Didier Dinks to do this + if float(response[1]) >= interestLevelPrice: + currency.update(ctx.author.id, "dinks", float(response[1]) - interestLevelPrice) + currency.update(ctx.author.id, "banklevel", int(response[2]) + 1) + await ctx.send("**{}** heeft zijn bank geüpgradet naar level **{}**!" + .format(ctx.author.display_name, str(int(response[2]) + 1))) + else: + await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**." + .format(ctx.author.display_name)) + + @upgrade.command(aliases=["Cap", "Capacity", "O", "Offence"], hidden=True) + async def offense(self, ctx): + """ + Command that upgrades the user's bank offense, + increasing capacity & rob chances. + :param ctx: Discord Context + """ + response = currency.getOrAddUser(ctx.author.id) + + offense = int(response[7]) + capacity = calcCapacity(offense) + offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity + + # Check if user has enough Didier Dinks to do this + if float(response[1]) >= offenseLevelPrice: + currency.update(ctx.author.id, "dinks", float(response[1]) - offenseLevelPrice) + currency.update(ctx.author.id, "offense", int(response[7]) + 1) + await ctx.send("**{}** heeft de offense van zijn bank geüpgradet naar level **{}**!" + .format(ctx.author.display_name, int(response[7]) + 1)) + else: + await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**." + .format(ctx.author.display_name)) + + @upgrade.command(aliases=["D", "Defence", "Def", "Security"], hidden=True) + async def defense(self, ctx): + """ + Command that upgrades the user's bank defense, + increasing chance of failed robs by others. + :param ctx: Discord Context + """ + response = currency.getOrAddUser(ctx.author.id) + defense = int(response[6]) + defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6) + + # Check if user has enough Didier Dinks to do this + if float(response[1]) >= defenseLevelPrice: + currency.update(ctx.author.id, "dinks", float(response[1]) - defenseLevelPrice) + currency.update(ctx.author.id, "defense", int(response[6]) + 1) + await ctx.send("**{}** heeft de security van zijn bank geüpgradet naar level **{}**!" + .format(ctx.author.display_name, int(response[6]) + 1)) + else: + await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**." + .format(ctx.author.display_name)) + + @commands.command(name="Invest", aliases=["Deposit"], usage="[Aantal]") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def invest(self, ctx, *amount: Abbreviated): + """ + Command that invests Didier Dinks into the user's bank. + :param ctx: Discord Context + :param amount: the amount of Didier Dinks to invest + """ + # Tuples don't support assignment + amount = list(amount) + + if len(amount) != 1: + await ctx.send("Geef een geldig bedrag op.") + elif not checks.isValidAmount(ctx, amount[0])[0]: + await ctx.send(checks.isValidAmount(ctx, amount[0])[1]) + else: + user = currency.getOrAddUser(ctx.author.id) + if amount[0] == "all": + amount[0] = user[1] + + amount[0] = float(amount[0]) + currency.update(ctx.author.id, "investedamount", float(user[3]) + amount[0]) + currency.update(ctx.author.id, "dinks", float(user[1]) - amount[0]) + await ctx.send("**{}** heeft **{:,}** Didier Dink{} geïnvesteerd!" + .format(ctx.author.display_name, math.floor(amount[0]), checks.pluralS(amount[0]))) + + @commands.command(name="Claim", usage="[Aantal]*") + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def claim(self, ctx, *args): + """ + Command that claims profit out of the user's Didier Bank. + :param ctx: + :param args: + :return: + """ + user = currency.getOrAddUser(ctx.author.id) + args = list(args) + claimAll = False + + if len(args) == 0: + args.append("all") + if args[0] == "all": + args[0] = float(user[5]) + claimAll = True + + if not claimAll: + args[0] = abbreviated(str(args[0])) + if args[0] is None: + return await ctx.send("Dit is geen geldig bedrag.") + + try: + # Checks if it can be parsed to int + _ = int(args[0]) + args[0] = float(args[0]) + # Can't claim more than you have (or negative amounts) + if args[0] < 0 or args[0] > float(user[5]): + raise ValueError + + currency.update(ctx.author.id, "profit", float(user[5]) - args[0]) + currency.update(ctx.author.id, "dinks", float(user[1]) + args[0]) + s = stats.getOrAddUser(ctx.author.id) + stats.update(ctx.author.id, "profit", float(s[7]) + args[0]) + + # If you claim everything, you get your invest back as well & your days reset + if claimAll: + currency.update(ctx.author.id, "dinks", float(user[1]) + float(user[3]) + float(user[5])) + currency.update(ctx.author.id, "investedamount", 0.0) + currency.update(ctx.author.id, "investeddays", 0) + await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!" + .format(ctx.author.display_name, math.floor(args[0] + float(user[3])), + checks.pluralS(math.floor(args[0] + float(user[3]))))) + else: + await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!".format( + ctx.author.display_name, math.floor(args[0]), checks.pluralS(math.floor(args[0])))) + + except ValueError: + await ctx.send("Geef een geldig bedrag op.") + + @commands.group(name="Rob", usage="[@Persoon]", case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def rob(self, ctx, target: discord.User): + """ + Command to rob another user. + :param ctx: Discord Context + :param target: the target victim to be robbed + :return: + """ + canRob, caller, target = await self.canRob(ctx, target) + + if not canRob: + return + + threshold = 50 + round(int(target[6]) * 0.7) + rg = random.randint(0 + int(caller[7]), 100) + stat = stats.getOrAddUser(ctx.author.id) + + # Rob succeeded + if rg > threshold: + capacity = float(calcCapacity(caller[7])) + remaining = capacity + + # Try robbing out of invest first, then Dinks pouch + amount = capacity if float(target[3]) >= capacity else float(target[3]) + remaining -= amount + currency.update(target[0], "investedamount", float(target[3]) - amount) + + # Rob out of Dinks pouch + if amount != capacity and not float(target[1]) < 1: + if float(target[1]) >= remaining: + amount += remaining + currency.update(target[0], "dinks", float(target[1]) - remaining) + else: + amount += float(target[1]) + currency.update(target[0], "dinks", 0.0) + + # Update db + currency.update(caller[0], "dinks", float(caller[1]) + amount) + await ctx.send("**{}** heeft **{:,}** Didier Dink{} gestolen van **{}**!".format( + ctx.author.display_name, math.floor(amount), checks.pluralS(math.floor(amount)), + self.utilsCog.getDisplayName(ctx, target[0]) + )) + + stats.update(ctx.author.id, "robs_success", int(stat[2]) + 1) + stats.update(ctx.author.id, "robs_total", float(stat[4]) + amount) + else: + # Rob failed + + # Calculate what happens + fate = random.randint(1, 10) + + # Leave Dinks behind instead of robbing + if fate < 8: + punishment = float(calcCapacity(caller[7]))/2 + prisoned = round(float(caller[1])) < round(punishment) + + # Doesn't have enough Dinks -> prison + if prisoned: + diff = round(punishment - float(caller[1])) + punishment = round(float(caller[1])) + days = 1 + round(int(caller[7]) // 10) + prison.imprison(caller[0], diff, days, + round(round(diff)//days)) + + # Update db + currency.update(target[0], "dinks", float(target[1]) + punishment) + currency.update(caller[0], "dinks", float(caller[1]) - punishment) + await ctx.send("**{}** was zo vriendelijk om **{}** zowaar **{:,}** Didier Dink{} te geven!" + .format(ctx.author.display_name, + self.utilsCog.getDisplayName(ctx, target[0]), + math.floor(punishment), checks.pluralS(math.floor(punishment)))) + + # Can't put this in the previous if- because the value of Punishment changes + if prisoned: + await ctx.send("Je bent naar de gevangenis verplaatst omdat je niet genoeg Didier Dinks had.") + elif fate == 9: + # Prison + totalSum = round(calcCapacity(caller[7])) + days = 1 + (int(caller[7])//10) + + prison.imprison(caller[0], totalSum, days, totalSum/days) + await ctx.send("**{}** niet stelen, **{}** niet stelen!\nJe bent naar de gevangenis verplaatst.".format( + ctx.author.display_name, ctx.author.display_name + )) + else: + # Escape + await ctx.send("Je poging is mislukt, maar je kon nog net op tijd vluchten, **{}**.".format( + ctx.author.display_name)) + + stats.update(ctx.author.id, "robs_failed", int(stat[3]) + 1) + + @rob.command(name="Leaderboard", aliases=["Lb", "Leaderboards"], hidden=True) + async def rob_leaderboard(self, ctx): + """ + Command that shows the Rob Leaderboard. + Alias for Lb Rob. + :param ctx: Discord Context + """ + await self.client.get_cog("Leaderboards").callLeaderboard("rob", ctx) + + @rob.command(name="Stats", hidden=True) + async def rob_stats(self, ctx): + """ + Command that shows the user's rob stats. + Alias for Stats Rob. + :param ctx: Discord Context + """ + await self.client.get_cog("Stats").callStats("rob", ctx) + + @commands.command(name="Prison", aliases=["Jail"]) + @commands.check(checks.allowedChannels) + @help.Category(category=Category.Currency) + async def prison(self, ctx): + """ + Command that shows how long you have to sit in prison for. + :param ctx: Discord Context + """ + user = prison.getUser(ctx.author.id) + + if len(user) == 0: + await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name)) + return + + user = user[0] + + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="De Gevangenis") + embed.add_field(name="Borgsom:", value="{:,}".format(math.floor(user[1])), inline=False) + embed.add_field(name="Resterende dagen:", value="{}".format((user[2])), inline=False) + await ctx.send(embed=embed) + + @commands.command(name="Bail") + @commands.check(checks.allowedChannels) + @help.Category(Category.Currency) + async def bail(self, ctx): + """ + Command to bail yourself out of prison. + :param ctx: Discord Context + """ + user = prison.getUser(ctx.author.id) + if len(user) == 0: + return await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name)) + + user = user[0] + + # Check if user can afford this + valid = checks.isValidAmount(ctx, math.floor(user[1])) + if not valid[0]: + return await ctx.send(valid[1]) + + dinks = currency.dinks(ctx.author.id) + prison.remove(ctx.author.id) + currency.update(ctx.author.id, "dinks", float(dinks) - float(user[1])) + await ctx.send("**{}** heeft zichzelf vrijgekocht!".format(ctx.author.display_name)) + + # Update the user's stats + s = stats.getOrAddUser(ctx.author.id) + stats.update(ctx.author.id, "bails", int(s[10]) + 1) + + # Increase the bail in the stats file + with open("files/stats.json", "r") as fp: + s = json.load(fp) + + s["rob"]["bail_paid"] += float(user[1]) + + with open("files/stats.json", "w") as fp: + json.dump(s, fp) + + async def canRob(self, ctx, target): + """ + Function that performs checks to see if a user can rob another user. + In case the rob is not possible, it already sends an error message to show this. + Returns the database dictionaries corresponding to these two users as they are + needed in this function anyways, so it prevents an unnecessary database call + in the rob command. + :param ctx: Discord Context + :param target: the target victim to be robbed + :return: success: boolean, user1 ("Caller"): tuple, user2 ("Target"): tuple + """ + # Can't rob in DM's + if str(ctx.channel.type) == "private": + await ctx.send("Dat doe je niet, {}.".format(ctx.author.display_name)) + return False, None, None + + # Can't rob bots + if str(ctx.author.id) in constants.botIDs: + await ctx.send("Nee.") + + # Can't rob in prison + if len(prison.getUser(ctx.author.id)) != 0: + await ctx.send("Je kan niemand bestelen als je in de gevangenis zit.") + return False, None, None + + # Check the database for these users + user1 = currency.getOrAddUser(ctx.author.id) + user2 = currency.getOrAddUser(target.id) + + # Can't rob without Didier Dinks + if float(user1[1]) < 1.0: + await ctx.send("Mensen zonder Didier Dinks kunnen niet stelen.") + return False, None, None + + # Target has no Didier Dinks to rob + if float(user2[1]) < 1.0 and float(user2[3]) < 1.0: + await ctx.send("Deze persoon heeft geen Didier Dinks om te stelen.") + return False, None, None + + # Passed all tests + return True, user1, user2 + + +def setup(client): + client.add_cog(Dinks(client)) diff --git a/cogs/events.py b/cogs/events.py new file mode 100644 index 0000000..909f9e5 --- /dev/null +++ b/cogs/events.py @@ -0,0 +1,336 @@ +from data import constants +import datetime +import discord +from discord.ext import commands +from functions import checks, easterEggResponses +from functions.database import stats, muttn +import pytz +import time +import traceback + + +class Events(commands.Cog): + + def __init__(self, client): + self.client = client + self.utilsCog = self.client.get_cog("Utils") + self.failedChecksCog = self.client.get_cog("FailedChecks") + self.lastFeatureRequest = 0 + self.lastBugReport = 0 + + @commands.Cog.listener() + async def on_connect(self): + """ + Function called when the bot connects to Discord. + """ + print("Connected") + + @commands.Cog.listener() + async def on_ready(self): + """ + Function called when the bot is ready & done leading. + """ + # Change status + with open("files/status.txt", "r") as statusFile: + status = statusFile.readline() + + await self.client.change_presence(status=discord.Status.online, activity=discord.Game(str(status))) + + # Print a message in the terminal to show that he's ready + with open("files/readyMessage.txt", "r") as readyFile: + readyMessage = readyFile.readline() + + print(readyMessage) + + # Add constants to the client as a botvar + self.client.constants = constants.Live if "zandbak" not in readyMessage else constants.Zandbak + + @commands.Cog.listener() + async def on_message(self, message): + """ + Function called when someone sends a message the bot can see. + :param message: the discord.Message instance of the message + """ + # Check if the server is locked, if so only allow me (to unlock) & Didier (to send the message) to talk + if self.client.locked \ + and message.guild is not None \ + and str(message.author.id) != constants.myId \ + and str(message.author.id) != constants.didierId: + # Auto unlock when someone sends a message past the current time + if time.time() > self.client.lockedUntil: + return await self.unlock(message.channel) + + return await self.utilsCog.removeMessage(message) + + # If FreeGamesCheck failed, remove the message & send the user a DM + if not checks.freeGamesCheck(message): + await self.failedChecksCog.freeGames(message) + + # Log commands in terminal + if any(message.content.lower().startswith(pre) for pre in self.client.prefixes): + DM = message.guild is None + print("{} in {}: {}".format(message.author.display_name, + "DM" if DM else "{} ({})".format(message.channel.name, message.guild.name), + message.content)) + + # Boos React to people that call him Dider + if "dider" in message.content.lower() and str(message.author.id) not in [constants.myId, constants.didierId]: + await message.add_reaction("<:boos:629603785840263179>") + + # Check for other easter eggs + eER = easterEggResponses.control(message) + if eER: + await message.channel.send(eER) + + # Earn XP & Message count + stats.sentMessage(message) + + @commands.Cog.listener() + async def on_command_error(self, ctx, err): + """ + Function called when a command throws an error. + :param ctx: Discord Context + :param err: the error thrown + """ + # Don't handle commands that have their own custom error handler + if hasattr(ctx.command, 'on_error'): + return + # Someone just mentioned Didier without calling a real command, + # don't care about this error + if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments), ): + pass + # Someone used a command that was on cooldown + elif isinstance(err, commands.CommandOnCooldown): + await ctx.send("Je kan dit commando niet (meer) spammen.", delete_after=10) + # Someone forgot an argument or passed an invalid argument + elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument)): + await ctx.send("Controleer je argumenten.") + else: + # Remove the InvokeCommandError because it's useless information + x = traceback.format_exception(type(err), err, err.__traceback__) + errorString = "" + for line in x: + if "direct cause of the following" in line: + break + errorString += line.replace("*", "") + "\n" if line.strip() != "" else "" + await self.sendErrorEmbed(ctx, err, errorString) + + @commands.Cog.listener() + async def on_raw_reaction_add(self, react): + """ + Function called when someone adds a reaction to a message. + :param react: the RawReactionEvent associated with the reaction + """ + # Ignore RPS adding reacts + if self.client.get_user(react.user_id).bot: + return + # Feature request + if str(react.emoji) == "➕": + await self.sendReactEmbed(react, "Feature Request") + # Bug report + elif str(react.emoji) == "🐛": + await self.sendReactEmbed(react, "Bug Report") + # Muttn react + elif str(react.emoji) == "<:Muttn:761551956346798111>": + await self.addMuttn(react) + + @commands.Cog.listener() + async def on_raw_reaction_remove(self, react): + """ + Function called when someone removes a reaction from a message. + :param react: the RawReactionEvent associated with the reaction + """ + # Decrease Muttn counter + if str(react.emoji) == "<:Muttn:761551956346798111>": + await self.removeMuttn(react) + + async def removeMuttn(self, react): + """ + Function that decreases the Muttn counter for someone. + :param react: the RawReactionEvent associated with the reaction + """ + # Get the Message instance of the message + channel = self.client.get_channel(react.channel_id) + message = await channel.fetch_message(react.message_id) + muttn.removeMuttn(message) + + async def addMuttn(self, react): + """ + Function that checks the Muttn counter for a message. + :param react: the RawReactionEvent associated with the reaction + """ + count = -1 + # Get the Message instance of the message + channel = self.client.get_channel(react.channel_id) + message = await channel.fetch_message(react.message_id) + + # Get the amount of reacts on this message + for reaction in message.reactions: + if str(reaction.emoji) == "<:Muttn:761551956346798111>": + count = reaction.count + for user in await reaction.users().flatten(): + # Remove bot reacts + if user.bot: + count -= 1 + break + + # React was removed in the milliseconds the fetch_message needs to get the info + if count <= 0: + return + + # Update the db + muttn.muttn(message.author.id, count, message.id) + + def reactCheck(self, react, msg): + """ + Function that checks if feature requests/bug reports have been sent already. + :param react: the RawReactionEvent associated with the reaction + :param msg: the message this react was placed on + """ + # Blacklist NinjaJay after spamming + if react.user_id in [153162010576551946]: + return False + + # Don't spam DM's when something has already been reported + # Check if the react's count is 1 + for reaction in msg.reactions: + if reaction.emoji == react.emoji.name: + return reaction.count == 1 + + async def sendReactEmbed(self, react, messageType): + """ + Function that sends a message in Zandbak with what's going on. + :param react: the RawReactionEvent associated with the reaction + :param messageType: the type of message to send + """ + channel = self.client.get_channel(react.channel_id) + msg = await channel.fetch_message(react.message_id) + + # Didn't pass the check, ignore it + if not self.reactCheck(react, msg): + return + + typeChannels = {"Feature Request": int(constants.FeatureRequests), "Bug Report": int(constants.BugReports)} + + # Add a 10 second cooldown to requests/reports to avoid spam + # even tho apparently the people don't care + if round(time.time()) - ( + self.lastFeatureRequest if messageType == "Feature Request" else self.lastBugReport) < 10: + await channel.send("Je moet even wachten vooraleer je nog een {} maakt.".format(messageType.lower())) + await msg.add_reaction("🕐") + return + # Report on an empty message + elif msg.content == "": + await channel.send("Dit bericht bevat geen tekst.") + await msg.add_reaction("❌") + return + + # Update the variables + if messageType == "Feature Request": + self.lastFeatureRequest = round(time.time()) + else: + self.lastBugReport = round(time.time()) + + # Ignore people reacting on Didier's messages + if str(msg.author.id) != constants.didierId: + # Get the user's User instance & the channel to send the message to + COC = self.client.get_guild(int(constants.CallOfCode)) + user = COC.get_member(react.user_id) + targetChannel = self.client.get_channel(typeChannels[messageType]) + + await targetChannel.send("{} door **{}** in **#{}** ({}):\n``{}``\n{}".format( + messageType, + user.display_name, + channel.name if str(channel.type) != "private" else "DM", + channel.guild.name if str(channel.type) != "private" else "DM", + msg.content, msg.jump_url + )) + await msg.add_reaction("✅") + + @commands.Cog.listener() + async def on_message_edit(self, before, after): + """ + Function called when a message is edited, + so people can't edit messages in FreeGames to cheat the system. + :param before: the message before it was edited + :param after: the message after it was edited + """ + # Run the message through the checks again + if not checks.freeGamesCheck(after): + await self.failedChecksCog.freeGames(after) + + async def sendErrorEmbed(self, ctx, error: Exception, trace): + """ + Function that sends an error embed in #ErrorLogs. + :param ctx: Discord Context + :param error: the error thrown + :param trace: the stacktrace of the error + """ + embed = discord.Embed(colour=discord.Colour.red()) + embed.set_author(name="Error") + embed.add_field(name="Command:", value="{} in {}: {}".format(ctx.author.display_name, + ctx.channel.name if str( + ctx.channel.type) != "private" else "DM", + ctx.message.content), inline=False) + embed.add_field(name="Error:", value=str(error)[:1024], inline=False) + embed.add_field(name="Message:", value=str(trace)[:1024], inline=False) + + # Add remaining parts in extra fields + # (embed field limits) + if len(str(trace)) < 5500: + trace_split = [str(trace)[i:i + 1024] for i in range(1024, len(str(trace)), 1024)] + for spl in trace_split: + embed.add_field(name="\u200b", value=spl, inline=False) + + errorChannel = self.client.get_channel(762668505455132722) + await errorChannel.send(embed=embed) + + @commands.command(hidden=True) + @commands.check(checks.isMe) + async def lock(self, ctx, until=None): + """ + Command that locks the server during online exams. + :param ctx: Discord Context + :param until: the timestamp until which to lock (HH:MM) + """ + # No timestamp passed + if until is None: + return + + until = until.split(":") + + # Create timestamps + now = datetime.datetime.now() + untilTimestamp = time.time() + + # Gets the current amount of minutes into the day + nowMinuteCount = (now.hour * 60) + now.minute + + # Gets the target amount of minutes into the day + untilMinuteCount = (int(until[0]) * 60) + int(until[1]) + + # Adds the remaining seconds onto the current time to calculate the end of the lock + untilTimestamp += (60 * (untilMinuteCount - nowMinuteCount)) - now.second + + self.client.locked = True + self.client.lockedUntil = round(untilTimestamp) + + await ctx.send("De server wordt gelocked tot **{}**.".format( + datetime.datetime.fromtimestamp(untilTimestamp, + pytz.timezone("Europe/Brussels") + ).strftime('%H:%M:%S'))) + + @commands.command(hidden=True) + @commands.check(checks.isMe) + async def unlock(self, ctx): + """ + Command to unlock the server manually before the timer is over. + :param ctx: Discord Context + """ + self.client.locked = False + self.client.lockedUntil = -1 + await ctx.send("De server is niet langer gelocked.") + + +def setup(client): + client.add_cog(Events(client)) diff --git a/cogs/failedchecks.py b/cogs/failedchecks.py new file mode 100644 index 0000000..987dc1a --- /dev/null +++ b/cogs/failedchecks.py @@ -0,0 +1,28 @@ +from data import constants +from discord.ext import commands + + +# Cog that handles failure of checks +# Has to be a Cog to have access to the Client +class FailedChecks(commands.Cog): + + def __init__(self, client): + self.client = client + self.utilsCog = self.client.get_cog("Utils") + + # User posted in #FreeGames without being allowed to do so + async def freeGames(self, ctx): + content = ctx.content + errorChannel = self.client.get_channel(int(constants.ErrorLogs)) + + await self.utilsCog.removeMessage(ctx) + await self.utilsCog.sendDm(ctx.author.id, + "Je bericht \n`{}`\n werd verwijderd uit #FreeGames omdat het geen link " + "bevatte.\nPost AUB enkel links in dit kanaal.\n*Als je bericht onterecht " + "verwijderd werd, stuur dan een DM naar DJ STIJN.*".format(content)) + await errorChannel.send("`{}`\nDoor **{}** werd verwijderd uit #FreeGames.".format(content, + ctx.author.display_name)) + + +def setup(client): + client.add_cog(FailedChecks(client)) diff --git a/cogs/faq.py b/cogs/faq.py new file mode 100644 index 0000000..da18a80 --- /dev/null +++ b/cogs/faq.py @@ -0,0 +1,170 @@ +from decorators import help +import discord +from discord.ext import commands +from enums.help_categories import Category +from functions import stringFormatters, checks +from functions.database import faq + + +class Faq(commands.Cog): + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return not self.client.locked + + @commands.group(name="FAQ", usage="[Categorie]* [@Personen]*", case_insensitive=True, invoke_without_command=True) + @help.Category(category=Category.Other) + async def faq(self, ctx, *args): + """ + Command group that controls the FAQ commands. + When this command is invoked, it sends a list of valid categories. + :param ctx: Discord Context + :param args: args passed + """ + # A category was requested + # This is not the cleanest but 80 subcommands is a bit much + if len(args) != 0 and any("@" not in arg for arg in args): + return await self.faqCategory(ctx, args) + + # List of all categories with the first letter capitalized + resp = [stringFormatters.titleCase(cat[0]) for cat in faq.getCategories()] + + # Sort alphabetically + resp.sort() + + # Create an embed with all the categories + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="FAQ Categorieën") + embed.description = "\n".join(resp) + + # Check if the embed has to be sent to the user + # or if the user tagged anyone + if len(ctx.message.mentions) == 0: + await ctx.author.send(embed=embed) + else: + embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name)) + # Send it to everyone that was mentioned + for person in ctx.message.mentions: + if not person.bot: + await person.send(embed=embed) + + @faq.command(hidden=True, name="Add", usage="[Category] [Question]* [Answer]*") + @commands.check(checks.isMe) + async def add(self, ctx, category, question=None, answer=None, answer_markdown=None): + """ + Command to add a FAQ to the database + :param ctx: Discord Context + :param category: the category to add the FAQ to + :param question: the question + :param answer: the answer + :param answer_markdown: a version of the answer with markdown applied + """ + # Add a new category + if question is None or answer is None: + faq.addCategory(category) + await ctx.send("**{}** is toegevoegd.".format(category)) + else: + # Add a new question/answer couple to a category + faq.addQuestion(category, question, answer, answer_markdown) + await ctx.send("``{}\n{}`` is toegevoegd in {}.".format(question, answer, category)) + + # Quotes a specific line of the fac instead of DM'ing the entire thing + @faq.command(name="Quote", aliases=["Q"], usage="[Categorie] [Index]") + @help.Category(category=Category.Other) + async def quote(self, ctx, category, index): + """ + Command that quotes 1 line of the FAQ into the current channel. + :param ctx: Discord Context + :param category: the category of the FAQ + :param index: the index in the list to quote + :return:y + """ + # Check if a (valid) number was passed + try: + index = int(index) + if index < 1: + raise ValueError + except ValueError: + await ctx.send("Dit is geen geldig getal.") + + # Create a list of categories + categories = [t[0] for t in faq.getCategories()] + + # Check if a valid category was passed + if category.lower() not in categories: + return await ctx.send("Dit is geen geldige categorie.") + + resp = faq.getCategory(category.lower()) + + # Check if this index exists in this category + if len(resp) < index: + return await ctx.send("Dit is geen geldig getal.") + + # Sort by entry Id + resp.sort(key=lambda x: int(x[0])) + + await ctx.send("**{}**\n{}".format(resp[index - 1][2], resp[index - 1][3])) + + async def faqCategory(self, ctx, args): + """ + Function that sends everything from a category. + :param ctx: Discord Context + :param args: the args passed + """ + + # Create a list of categories + categories = [t[0] for t in faq.getCategories()] + + # Random word was passed as a category + if not any(arg.lower() in categories for arg in args): + return await self.sendErrorEmbed(ctx, "Dit is geen geldige categorie.") + elif len(args) - len(ctx.message.mentions) != 1: + # Multiple categories were requested, which is not allowed + return await self.sendErrorEmbed(ctx, "Controleer je argumenten.") + + category = "" + + # Find the category the user requested + for word in args: + if word.lower() in categories: + category = word + break + + resp = faq.getCategory(category.lower()) + + # Sort by entry Id + resp.sort(key=lambda x: int(x[0])) + + embed = discord.Embed(colour=discord.Colour.blue()) + embed.set_author(name="FAQ {}".format(stringFormatters.titleCase(category))) + + # Add everything into the embed + for i, pair in enumerate(resp): + # Add custom markdown if it exists + embed.add_field(name="#{}: {}".format(str(i + 1), pair[2]), value=pair[3] if pair[4] is None else pair[4], inline=False) + + # Check if anyone was tagged to send the embed to + if len(ctx.message.mentions) == 0: + await ctx.author.send(embed=embed) + else: + embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name)) + # Author tagged some people to send it to + for person in ctx.message.mentions: + await person.send(embed=embed) + + async def sendErrorEmbed(self, ctx, message: str): + """ + Function that sends an error embed. + :param ctx: Discord Context + :param message: the message to put in the embed + """ + embed = discord.Embed(colour=discord.Colour.red()) + embed.set_author(name="Error") + embed.description = message + await ctx.send(embed=embed) + + +def setup(client): + client.add_cog(Faq(client)) diff --git a/cogs/football.py b/cogs/football.py new file mode 100644 index 0000000..b025543 --- /dev/null +++ b/cogs/football.py @@ -0,0 +1,89 @@ +from bs4 import BeautifulSoup +import datetime +from decorators import help +from discord.ext import commands +from enums.help_categories import Category +from functions import checks, config +import requests +import tabulate + + +class Football(commands.Cog): + def __init__(self, client): + self.client = client + + # Don't allow any commands to work when locked + def cog_check(self, ctx): + return checks.isMe(ctx) and not self.client.locked + + @commands.group(name="Jpl", case_insensitive=True, invoke_without_command=True) + @commands.check(checks.allowedChannels) + @help.Category(Category.Sports) + async def jpl(self, ctx, *args): + pass + + @jpl.command(name="Matches", aliases=["M"], usage="[Week]*") + async def matches(self, ctx, *args): + args = list(args) + if not args: + args = [str(config.get("jpl_day"))] + if all(letter.isdigit() for letter in args[0]): + current_day = requests.get("https://api.sporza.be/web/soccer/matchdays/161733/{}".format(args[0])).json() + current_day = current_day["groupedMatches"][0]["matches"] + + # Create dictionaries for every match + matches_formatted = {} + for i, match in enumerate(current_day): + matchDic = {"home": match["homeTeam"]["name"], "away": match["awayTeam"]["name"]} + + # Add date + matchDate = datetime.datetime.strptime(match["startDateTime"].split("+")[0], "%Y-%m-%dT%H:%M:%S.%f") + matchDic["date"] = matchDate.strftime("%d/%m") + matchDic["day"] = self.get_weekday(matchDate.weekday()) + + # TODO check back when there's active games (to find the key in the dict) & add the current time if not over + # Add scores + if match["status"] == "END": # Status != [not_yet_started] whatever it is + matchDic["score"] = "{} - {}".format(match["homeScore"], match["awayScore"]) + else: + # If there's no score, show when the match starts + matchDic["score"] = "{}:{}".format( + ("" if len(str(matchDate.hour)) == 2 else "0") + str(matchDate.hour), # Leading Zero + ("" if len(str(matchDate.minute)) == 2 else "0") + str(matchDate.minute)) # Leading Zero + + matches_formatted[i] = matchDic + + # Put every formatted version of the matches in a list + matchList = list([self.format_match(matches_formatted[match]) for match in matches_formatted]) + await ctx.send("```Jupiler Pro League - Speeldag {}\n\n{}```".format(args[0], tabulate.tabulate(matchList, headers=["Dag", "Datum", "Thuis", "Stand", "Uit", "Tijd"]))) + else: + return await ctx.send("Dit is geen geldige speeldag.") + + # TODO check back when there's active games & add the timestamp instead of EINDE + def format_match(self, match): + return [match["day"], match["date"], match["home"], match["score"], match["away"], "Einde"] + + def get_weekday(self, day: int): + days = ["Ma", "Di", "Wo", "Do", "Vr", "Za", "Zo"] + return days[day] + + @jpl.command(name="Table", aliases=["Ranking", "Rankings", "Ranks", "T"]) + async def table(self, ctx, *args): + page_html = requests.get("https://sporza.be/nl/categorie/voetbal/jupiler-pro-league/").text + bs_parsed = BeautifulSoup(page_html, "html.parser") + rows = bs_parsed.find(summary="algemeen klassement").find_all("tr")[1:] + rowsFormatted = [] + for row in rows: + rowsFormatted.append(self.createRowList(row)) + await ctx.send("```Jupiler Pro League Klassement\n\n{}```".format(tabulate.tabulate(rowsFormatted, headers=["#", "Ploeg", "Punten", "M", "M+", "M-", "M="]))) + + # Formats the row into an list that can be passed to Tabulate + def createRowList(self, row): + scoresArray = list([td.renderContents().decode("utf-8") for td in row.find_all("td")])[:6] + # Insert the team name into the list + scoresArray.insert(1, row.find_all("a")[0].renderContents().decode("utf-8").split("