From fe74128512b83e5a90fcfb5c655d9bc9fceab56a Mon Sep 17 00:00:00 2001 From: kwzrd Date: Sun, 23 Feb 2020 19:55:44 +0100 Subject: Add icon cycle frequency constant --- bot/constants.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index f0656926..52a4aa20 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -69,6 +69,7 @@ class Client(NamedTuple): token = environ.get("SEASONALBOT_TOKEN") debug = environ.get("SEASONALBOT_DEBUG", "").lower() == "true" season_override = environ.get("SEASON_OVERRIDE") + icon_cycle_frequency = 3 # N days to wait between cycling server icons within a single season class Colours: -- cgit v1.2.3 From 829d32c75ae67d42da884dafc7bfc9476e9f086d Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 24 Feb 2020 18:48:09 +0200 Subject: Added .games command with all it's subcommands, added IGDB token requirement to constants.py. --- bot/constants.py | 1 + bot/seasons/evergreen/game.py | 338 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 bot/seasons/evergreen/game.py (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index f0656926..cb3228b6 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -134,6 +134,7 @@ class Tokens(NamedTuple): omdb = environ.get("OMDB_API_KEY") youtube = environ.get("YOUTUBE_API_KEY") tmdb = environ.get("TMDB_API_KEY") + igdb = environ.get("IGDB_API_KEY") # Default role combinations diff --git a/bot/seasons/evergreen/game.py b/bot/seasons/evergreen/game.py new file mode 100644 index 00000000..d378c34e --- /dev/null +++ b/bot/seasons/evergreen/game.py @@ -0,0 +1,338 @@ +import asyncio +import random +from datetime import datetime +from enum import IntEnum +from typing import Any, Dict, List, Tuple + +from aiohttp import ClientSession +from discord import Embed +from discord.ext.commands import Bot, Cog, Context, group + +from bot.constants import Tokens +from bot.pagination import ImagePaginator, LinePaginator + +# Base URL of IGDB API +BASE_URL = "https://api-v3.igdb.com/" +IMAGE_BASE_URL = "https://images.igdb.com/igdb/image/upload/" + +HEADERS = { + "user-key": Tokens.igdb, + "Accept": "application/json" +} + + +class GameStatus(IntEnum): + """Game statuses in IGDB API.""" + + Released = 0 + Alpha = 2 + Beta = 3 + Early = 4 + Offline = 5 + Cancelled = 6 + Rumored = 7 + + +class AgeRatingCategories(IntEnum): + """IGDB API Age Rating categories IDs.""" + + ESRB = 1 + PEGI = 2 + + +class AgeRatings(IntEnum): + """PEGI/ESRB ratings IGDB API IDs.""" + + Three = 1 + Seven = 2 + Twelve = 3 + Sixteen = 4 + Eighteen = 5 + RP = 6 + EC = 7 + E = 8 + E10 = 9 + T = 10 + M = 11 + AO = 12 + + +class Games(Cog): + """Games Cog contains commands that collect data from IGDB.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.http_session: ClientSession = bot.http_session + + # Initialize genres + asyncio.get_event_loop().create_task(self._get_genres()) + + async def _get_genres(self) -> None: + """Create genres variable for games command.""" + body = "fields name; limit 100;" + async with self.http_session.get(BASE_URL + "genres", data=body, headers=HEADERS) as resp: + result = await resp.json() + + genres = {genre['name'].capitalize(): genre['id'] for genre in result} + + self.genres = {} + + # Manual check genres, replace sentences with words + for genre in genres: + if genre == "Role-playing (rpg)": + self.genres["Role-playing"] = genres[genre] + self.genres["Rpg"] = genres[genre] + elif genre == "Turn-based strategy (tbs)": + self.genres["Turn-based-strategy"] = genres[genre] + self.genres["Tbs"] = genres[genre] + elif genre == "Real time strategy (rts)": + self.genres["Real-time-strategy"] = genres[genre] + self.genres["Rts"] = genres[genre] + elif genre == "Hack and slash/beat 'em up": + self.genres["Hack-and-slash"] = genres[genre] + else: + self.genres[genre] = genres[genre] + + @group(name='games', aliases=['game'], invoke_without_command=True) + async def games(self, ctx: Context, genre: str = "", amount: int = 5) -> None: + """ + Get random game(s) by genre from IGDB. Use .movies genres command to get all available genres. + + Also support amount parameter, what max is 25 and min 1, default 5. Use quotes ("") for genres with multiple + words. + """ + # Capitalize genre for check + genre = genre.capitalize() + + # Check for amounts, max is 25 and min 1 + if amount > 25: + await ctx.send("You can't get more than 25 games at once.") + return + elif amount < 1: + await ctx.send("You can't get less than 1 game.") + return + + # Get games listing, if genre don't exist, show help. + try: + games = await self.get_games_list(self.http_session, amount, self.genres[genre], + offset=random.randint(0, 150)) + except KeyError: + await ctx.send_help('games') + return + + # Create pages and paginate + pages = await self.get_pages(games) + + await ImagePaginator.paginate(pages, ctx, Embed(title=f'Random {genre} Games')) + + @games.command(name='top', aliases=['t']) + async def top(self, ctx: Context, amount: int = 10) -> None: + """ + Get current Top games in IGDB. + + Support amount parameter. Max is 25, min is 1. + """ + if amount > 25: + await ctx.send("You can't get more than top 25 games.") + return + elif amount < 1: + await ctx.send("You can't get less than 1 top game.") + return + + games = await self.get_games_list(self.http_session, amount, sort='total_rating desc', + additional_body="where total_rating >= 90; sort total_rating_count desc;") + + pages = await self.get_pages(games) + await ImagePaginator.paginate(pages, ctx, Embed(title=f'Top {amount} Games')) + + @games.command(name='genres', aliases=['genre', 'g']) + async def genres(self, ctx: Context) -> None: + """Get all available genres.""" + await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}") + + @games.command(name='search', aliases=['s']) + async def search(self, ctx: Context, *, search: str) -> None: + """Find games by name.""" + lines = await self.search_games(self.http_session, search) + + await LinePaginator.paginate((line for line in lines), ctx, Embed(title=f'Game Search Results: {search}')) + + @games.command(name='company', aliases=['companies']) + async def company(self, ctx: Context, amount: int = 5) -> None: + """ + Get random Game Companies companies from IGDB API. + + Support amount parameter. Max is 25, min is 1. + """ + if amount > 25: + await ctx.send("You can't get more than 25 companies at once.") + return + elif amount < 1: + await ctx.send("You can't get less than 1 company.") + return + + companies = await self.get_companies_list(self.http_session, amount, random.randint(0, 150)) + pages = await self.get_company_pages(companies) + + await ImagePaginator.paginate(pages, ctx, Embed(title='Random Game Companies')) + + async def get_games_list(self, + client: ClientSession, + limit: int, + genre: str = None, + sort: str = None, + additional_body: str = "", + offset: int = 0) \ + -> List[Dict[str, Any]]: + """Get Games List from IGDB API.""" + # Create body of IGDB API request, define fields, sorting, offset, limit and genre + body = "fields cover.image_id, first_release_date, total_rating, name, storyline, url, platforms.name, " + body += "status, involved_companies.company.name, summary, age_ratings.category, age_ratings.rating, " + body += "total_rating_count;\n" + + body += f"sort {sort};\n" if sort else '' + body += f"offset {offset};\n" + body += f"limit {limit};\n" + + body += f"where genres = ({genre});" if genre else '' + body += additional_body + + # Do request to IGDB API, create headers, URL, define body, return result + async with client.get( + url=BASE_URL + "games", + data=body, + headers=HEADERS + ) as resp: + return await resp.json() + + async def get_pages(self, data: List[Dict[str, Any]]) -> List[Tuple[str, str]]: + """Generate all game pages, do additional requests to IGDB API.""" + pages = [] + [pages.append(await self.create_page(game)) for game in data] + return pages + + async def create_page(self, data: Dict[str, Any]) -> Tuple[str, str]: + """Create content of Game Page.""" + # Create page content variable, what will be returned + page = "" + + # If game have cover, generate URL of Cover, if not, let url empty + if 'cover' in data: + url = f"{IMAGE_BASE_URL}t_cover_big/{data['cover']['image_id']}.jpg" + else: + url = "" + + # Add title with hyperlink and check for storyline + page += f"**[{data['name']}]({data['url']})**\n" + page += data['summary'] + "\n\n" if 'summary' in data else "\n" + + # Add release date if key is in game information + if 'first_release_date' in data: + page += f"**Release Date:** {datetime.utcfromtimestamp(data['first_release_date']).date()}\n" + + # Add other information + page += f"**Rating:** {'{0:.2f}'.format(data['total_rating']) if 'total_rating' in data else '?'}/100 " + page += f":star: (based on {data['total_rating_count'] if 'total_rating_count' in data else '?'})\n" + + page += f"**Platforms:** " + page += f"{', '.join(pf['name'] for pf in data['platforms']) if 'platforms' in data else '?'}\n" + + page += f"**Status:** {GameStatus(data['status']).name if 'status' in data else '?'}\n" + + if 'age_ratings' in data: + rating = f"""{', '.join(AgeRatingCategories(age['category']).name + ' ' + AgeRatings(age['rating']).name + for age in data['age_ratings'])}""" + page += f"**Age Ratings:** {rating}\n" + + if 'involved_companies' in data: + companies = ', '.join(co['company']['name'] for co in data['involved_companies']) + else: + companies = "?" + page += f"**Made by:** {companies}\n" + + page += "\n" + page += data['storyline'] if 'storyline' in data else '' + + return page, url + + async def search_games(self, client: ClientSession, search: str) -> List[str]: + """Search game from IGDB API by string, return listing of pages.""" + lines = [] + + # Define request body of IGDB API request and do request + body = f"""fields name, url, storyline, total_rating, total_rating_count; +search "{search}"; +limit 50;""" + + async with client.get( + url=BASE_URL + "games", + data=body, + headers=HEADERS) as resp: + data = await resp.json() + + # Loop over games, format them to good format, make line and append this to total lines + for game in data: + line = "" + + # Add games name and rating, also attach URL to title + line += f"**[{game['name']}]({game['url']})**\n" + line += f"""{'{0:.2f}'.format(game['total_rating'] if 'total_rating' in game.keys() else 0)}/100 :star: (by { + game['total_rating_count'] if 'total_rating_count' in game.keys() else '?'} users)""" + + lines.append(line) + + return lines + + async def get_companies_list(self, client: ClientSession, limit: int, offset: int = 0) -> List[Dict[str, Any]]: + """Get random Game Companies from IGDB API.""" + # Create request body, define included fields, limit and offset, do request + body = f"""fields name, url, start_date, logo.image_id, developed.name, published.name, description; +limit {limit}; +offset {offset};""" + + async with client.get( + url=BASE_URL + "companies", + data=body, + headers=HEADERS + ) as resp: + return await resp.json() + + async def get_company_pages(self, data: List[Dict[str, Any]]) -> List[Tuple[str, str]]: + """Get all good formatted pages about Game Companies.""" + pages = [] + + # Loop over companies, add them to pages listing and return pages + [pages.append(await self.create_company_page(co)) for co in data] + + return pages + + async def create_company_page(self, data: Dict[str, Any]) -> Tuple[str, str]: + """Create good formatted Game Company page.""" + page = "" + + # Generate URL of company logo + url = f"{IMAGE_BASE_URL}t_logo_med/{data['logo']['image_id'] if 'logo' in data.keys() else ''}.png" + + # Add name and description of company, attach URL to title + page += f"[{data['name']}]({data['url']})\n" + page += data['description'] + "\n\n" if 'description' in data.keys() else '\n' + + # Add other information + if 'start_date' in data.keys(): + founded = datetime.utcfromtimestamp(data['start_date']).date() + else: + founded = "?" + page += f"**Founded:** {founded}\n" + + page += "**Developed:** " + page += f"{', '.join(game['name'] for game in data['developed']) if 'developed' in data.keys() else '?'}\n" + + page += "**Published:** " + page += f"{', '.join(game['name'] for game in data['published']) if 'published' in data.keys() else '?'}" + + return page, url + + +def setup(bot: Bot) -> None: + """Add/Load Games cog.""" + bot.add_cog(Games(bot)) -- cgit v1.2.3 From f22eaa7148030303e8cbf27ab92c6011d423e8b7 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Tue, 25 Feb 2020 10:12:54 -0500 Subject: Update devlog channel constant The log channels have become one --- bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 52a4aa20..006cf77f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -35,7 +35,7 @@ class Channels(NamedTuple): bot = 267659945086812160 checkpoint_test = 422077681434099723 devalerts = 460181980097675264 - devlog = int(environ.get("CHANNEL_DEVLOG", 548438471685963776)) + devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) devtest = 414574275865870337 help_0 = 303906576991780866 help_1 = 303906556754395136 -- cgit v1.2.3 From 10b63edcefa3dc53c833c0ce383b003823f7fa7c Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Sun, 1 Mar 2020 18:57:39 -0500 Subject: Strip references to seasonalbot chat Redirect any output to seasonalbot commands, where relevant --- bot/constants.py | 1 - bot/seasons/easter/__init__.py | 2 +- bot/seasons/easter/egg_facts.py | 2 +- bot/seasons/evergreen/issues.py | 5 ++--- bot/seasons/halloween/candy_collection.py | 8 ++++---- bot/seasons/halloween/halloween_facts.py | 2 +- bot/seasons/pride/__init__.py | 2 +- bot/seasons/pride/pride_facts.py | 2 +- bot/seasons/valentines/be_my_valentine.py | 2 +- docker-compose.yml | 1 - 10 files changed, 12 insertions(+), 15 deletions(-) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 006cf77f..d406bbd0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -52,7 +52,6 @@ class Channels(NamedTuple): off_topic_2 = 463035268514185226 python = 267624335836053506 reddit = 458224812528238616 - seasonalbot_chat = int(environ.get("CHANNEL_SEASONALBOT_CHAT", 542272993192050698)) seasonalbot_commands = int(environ.get("CHANNEL_SEASONALBOT_COMMANDS", 607247579608121354)) seasonalbot_voice = int(environ.get("CHANNEL_SEASONALBOT_VOICE", 606259004230074378)) staff_lounge = 464905259261755392 diff --git a/bot/seasons/easter/__init__.py b/bot/seasons/easter/__init__.py index 1d77b6a6..dd60bf5c 100644 --- a/bot/seasons/easter/__init__.py +++ b/bot/seasons/easter/__init__.py @@ -16,7 +16,7 @@ class Easter(SeasonBase): • You may see stuff like an Easter themed esoteric challenge, a celebration of Earth Day, or Easter-related micro-events for you to join. Stay tuned! - If you'd like to contribute, head on over to <#542272993192050698> and we will help you get + If you'd like to contribute, head on over to <#635950537262759947> and we will help you get started. It doesn't matter if you're new to open source or Python, if you'd like to help, we will find you a task and teach you what you need to know. """ diff --git a/bot/seasons/easter/egg_facts.py b/bot/seasons/easter/egg_facts.py index 9e6fb1cb..e66e25a3 100644 --- a/bot/seasons/easter/egg_facts.py +++ b/bot/seasons/easter/egg_facts.py @@ -34,7 +34,7 @@ class EasterFacts(commands.Cog): async def send_egg_fact_daily(self) -> None: """A background task that sends an easter egg fact in the event channel everyday.""" - channel = self.bot.get_channel(Channels.seasonalbot_chat) + channel = self.bot.get_channel(Channels.seasonalbot_commands) while True: embed = self.make_embed() await channel.send(embed=embed) diff --git a/bot/seasons/evergreen/issues.py b/bot/seasons/evergreen/issues.py index c7501a5d..fba5b174 100644 --- a/bot/seasons/evergreen/issues.py +++ b/bot/seasons/evergreen/issues.py @@ -3,11 +3,10 @@ import logging import discord from discord.ext import commands -from bot.constants import Channels, Colours, Emojis, WHITELISTED_CHANNELS +from bot.constants import Colours, Emojis, WHITELISTED_CHANNELS from bot.decorators import override_in_channel log = logging.getLogger(__name__) -ISSUE_WHITELIST = WHITELISTED_CHANNELS + (Channels.seasonalbot_chat,) BAD_RESPONSE = { 404: "Issue/pull request not located! Please enter a valid number!", @@ -22,7 +21,7 @@ class Issues(commands.Cog): self.bot = bot @commands.command(aliases=("pr",)) - @override_in_channel(ISSUE_WHITELIST) + @override_in_channel(WHITELISTED_CHANNELS) async def issue( self, ctx: commands.Context, number: int, repository: str = "seasonalbot", user: str = "python-discord" ) -> None: diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py index 64da7ced..490609dd 100644 --- a/bot/seasons/halloween/candy_collection.py +++ b/bot/seasons/halloween/candy_collection.py @@ -41,7 +41,7 @@ class CandyCollection(commands.Cog): if message.author.bot: return # ensure it's hacktober channel - if message.channel.id != Channels.seasonalbot_chat: + if message.channel.id != Channels.seasonalbot_commands: return # do random check for skull first as it has the lower chance @@ -64,7 +64,7 @@ class CandyCollection(commands.Cog): return # check to ensure it is in correct channel - if message.channel.id != Channels.seasonalbot_chat: + if message.channel.id != Channels.seasonalbot_commands: return # if its not a candy or skull, and it is one of 10 most recent messages, @@ -124,7 +124,7 @@ class CandyCollection(commands.Cog): ten_recent = [] recent_msg_id = max( message.id for message in self.bot._connection._messages - if message.channel.id == Channels.seasonalbot_chat + if message.channel.id == Channels.seasonalbot_commands ) channel = await self.hacktober_channel() @@ -155,7 +155,7 @@ class CandyCollection(commands.Cog): async def hacktober_channel(self) -> discord.TextChannel: """Get #hacktoberbot channel from its ID.""" - return self.bot.get_channel(id=Channels.seasonalbot_chat) + return self.bot.get_channel(id=Channels.seasonalbot_commands) async def remove_reactions(self, reaction: discord.Reaction) -> None: """Remove all candy/skull reactions.""" diff --git a/bot/seasons/halloween/halloween_facts.py b/bot/seasons/halloween/halloween_facts.py index f8610bd3..94730d9e 100644 --- a/bot/seasons/halloween/halloween_facts.py +++ b/bot/seasons/halloween/halloween_facts.py @@ -40,7 +40,7 @@ class HalloweenFacts(commands.Cog): @commands.Cog.listener() async def on_ready(self) -> None: """Get event Channel object and initialize fact task loop.""" - self.channel = self.bot.get_channel(Channels.seasonalbot_chat) + self.channel = self.bot.get_channel(Channels.seasonalbot_commands) self.bot.loop.create_task(self._fact_publisher_task()) def random_fact(self) -> Tuple[int, str]: diff --git a/bot/seasons/pride/__init__.py b/bot/seasons/pride/__init__.py index 75e90b2a..08df2fa1 100644 --- a/bot/seasons/pride/__init__.py +++ b/bot/seasons/pride/__init__.py @@ -16,7 +16,7 @@ class Pride(SeasonBase): • [Pride issues are now available for SeasonalBot on the repo](https://git.io/pythonpride). • You may see Pride-themed esoteric challenges and other microevents. - If you'd like to contribute, head on over to <#542272993192050698> and we will help you get + If you'd like to contribute, head on over to <#635950537262759947> and we will help you get started. It doesn't matter if you're new to open source or Python, if you'd like to help, we will find you a task and teach you what you need to know. """ diff --git a/bot/seasons/pride/pride_facts.py b/bot/seasons/pride/pride_facts.py index b705bfb4..5c19dfd0 100644 --- a/bot/seasons/pride/pride_facts.py +++ b/bot/seasons/pride/pride_facts.py @@ -33,7 +33,7 @@ class PrideFacts(commands.Cog): async def send_pride_fact_daily(self) -> None: """Background task to post the daily pride fact every day.""" - channel = self.bot.get_channel(Channels.seasonalbot_chat) + channel = self.bot.get_channel(Channels.seasonalbot_commands) while True: await self.send_select_fact(channel, datetime.utcnow()) await asyncio.sleep(24 * 60 * 60) diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py index a073e1bd..de97cb4e 100644 --- a/bot/seasons/valentines/be_my_valentine.py +++ b/bot/seasons/valentines/be_my_valentine.py @@ -96,7 +96,7 @@ class BeMyValentine(commands.Cog): emoji_1, emoji_2 = self.random_emoji() lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id) - channel = self.bot.get_channel(Channels.seasonalbot_chat) + channel = self.bot.get_channel(Channels.seasonalbot_commands) valentine, title = self.valentine_check(valentine_type) if user is None: diff --git a/docker-compose.yml b/docker-compose.yml index f2f4b056..30e8a109 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,6 @@ services: # - SEASONALBOT_ADMIN_ROLE_ID= # - CHANNEL_ANNOUNCEMENTS= # - CHANNEL_DEVLOG= - # - CHANNEL_SEASONALBOT_CHAT= # - SEASON_OVERRIDE= volumes: -- cgit v1.2.3