diff --git a/gwendolyn/exceptions.py b/gwendolyn/exceptions.py index c0e967b..faa327f 100644 --- a/gwendolyn/exceptions.py +++ b/gwendolyn/exceptions.py @@ -3,7 +3,7 @@ class NoToken(Exception): self.message = "No discord bot token has been set in the .env file" super().__init__(self.message) -class MongoCannotConnect(Exception): - def __init__(self) -> None: - self.message = "Cannot connect to the mongo client" +class CannotConnectToService(Exception): + def __init__(self, service: str) -> None: + self.message = f"Cannot connect to {service}" super().__init__(self.message) \ No newline at end of file diff --git a/gwendolyn/ext/better_netflix.py b/gwendolyn/ext/better_netflix.py new file mode 100644 index 0000000..2d8628a --- /dev/null +++ b/gwendolyn/ext/better_netflix.py @@ -0,0 +1,12 @@ +from interactions import Extension, slash_command, SlashContext +from gwendolyn.utils import PARAMS as params + +class BetterNeflixExtension(Extension): + """Contains the Better Netflix commands.""" + @slash_command(**params["better_netflix"]["movie"]) + async def movie(self, ctx: SlashContext): + await self.bot.better_netflix.movie(ctx) + + @slash_command(**params["better_netflix"]["show"]) + async def show(self, ctx: SlashContext): + await self.bot.better_netflix.show(ctx) \ No newline at end of file diff --git a/gwendolyn/ext/misc.py b/gwendolyn/ext/misc.py index 31a6e66..39f9f18 100644 --- a/gwendolyn/ext/misc.py +++ b/gwendolyn/ext/misc.py @@ -50,3 +50,7 @@ class MiscExtension(Extension): log_message = f"{ctx.author.display_name} tried to stop me!" self.bot.log(log_message, str(ctx.channel_id)) await ctx.send(f"I don't think I will, {ctx.author.display_name}") + + @slash_command(**params["misc"]["echo"]) + async def echo(self, ctx: SlashContext, text: str): + await ctx.send(text) \ No newline at end of file diff --git a/gwendolyn/funcs/__init__.py b/gwendolyn/funcs/__init__.py index 5dd63f1..5122c47 100644 --- a/gwendolyn/funcs/__init__.py +++ b/gwendolyn/funcs/__init__.py @@ -1,5 +1,6 @@ """A collection of all Gwendolyn functions.""" -__all__ = ["Other"] +__all__ = ["Other","BetterNetflix", "Sonarr", "Radarr", "TMDb"] -from .other import Other \ No newline at end of file +from .other import Other +from .better_netflix import Radarr, Sonarr, BetterNetflix, TMDb \ No newline at end of file diff --git a/gwendolyn/funcs/better_netflix/__init__.py b/gwendolyn/funcs/better_netflix/__init__.py new file mode 100644 index 0000000..dc57ab4 --- /dev/null +++ b/gwendolyn/funcs/better_netflix/__init__.py @@ -0,0 +1,5 @@ +"""Better Netflix functions for Gwendolyn.""" + +__all__ = ["BetterNetflix", "Sonarr", "Radarr", "TMDb"] + +from .better_netflix import BetterNetflix, Sonarr, Radarr, TMDb diff --git a/gwendolyn/funcs/better_netflix/better_netflix.py b/gwendolyn/funcs/better_netflix/better_netflix.py new file mode 100644 index 0000000..d55960b --- /dev/null +++ b/gwendolyn/funcs/better_netflix/better_netflix.py @@ -0,0 +1,159 @@ +from requests import get +from random import choice +import io + +from PIL import Image +from interactions import SlashContext, Embed, EmbedFooter, EmbedAttachment, File + +from gwendolyn.exceptions import CannotConnectToService + +class Service(): + def __init__(self, ip: str, port: str, api_key: str) -> None: + self.ip = ip + self.port = port + self.header = { + "accept": "application/json", + "X-Api-Key": api_key + } + + def test(self): + return get(f"http://{self.ip}:{self.port}/api", headers=self.header).ok + +class Radarr(Service): + def movies(self): + response = get(f"http://{self.ip}:{self.port}/api/v3/movie", headers=self.header) + if not response.ok: + return [] + + return [m for m in response.json() if m["hasFile"]] + +class Sonarr(Service): + def shows(self): + response = get(f"http://{self.ip}:{self.port}/api/v3/series", headers=self.header) + if not response.ok: + return [] + + return response.json() + +class TMDb(): + def __init__(self, api_token: str) -> None: + self.header = { + "accept": "application/json", + "Authorization": "Bearer "+api_token + } + + def test(self): + return get(f"https://api.themoviedb.org/3/account", headers=self.header).ok + + +class BetterNetflix(): + def __init__(self, radarr: Radarr, sonarr: Sonarr, tmdb: TMDb, bot) -> None: + self.radarr = radarr + self.sonarr = sonarr + self.tmdb = tmdb + self.bot = bot + + services = ["radarr","sonarr","tmdb"] + + for service in services: + if getattr(self,service).test(): + self.bot.log(f"Connected to {service}") + else: + raise CannotConnectToService(service) + + def _poster_color(self, poster_url: str): + response = get(poster_url, stream=True) + img = Image.open(io.BytesIO(response.content)) + img.thumbnail((150, 100)) + + # Reduce colors (uses k-means internally) + paletted = img.convert('P', palette=Image.ADAPTIVE, colors=6) + + buf = io.BytesIO() + img2 = paletted.convert("RGB") + img2.save("gwendolyn/resources/temp.jpg") + + # Find the color that occurs most often + palette = paletted.getpalette() + color_counts = sorted(paletted.getcolors(), reverse=True) + palette_index = color_counts[0][1] + dominant_color = palette[palette_index*3:palette_index*3+3] + + return dominant_color + + async def movie(self, ctx: SlashContext): + msg = await ctx.send("Finding a random movie...") + movies = self.radarr.movies() + if len(movies) == 0: + await msg.edit(content="Unable to find any movies") + return + + picked_movie = choice(movies) + description = "" + + if "imdb" in picked_movie['ratings']: + description += f"<:imdb:1301506320603676782> {picked_movie['ratings']['imdb']['value']}" + description += "\u1CBC\u1CBC" + + if "rottenTomatoes" in picked_movie['ratings']: + rt_value = picked_movie['ratings']['rottenTomatoes']['value'] + rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>" + description += f"{rt_icon} {rt_value}%" + + if description != "": + description += "\n\n" + + year = EmbedFooter(str(picked_movie["year"])) if "year" in picked_movie else None + has_poster = len(picked_movie["images"]) > 0 and "remoteUrl" in picked_movie["images"][0] + poster = EmbedAttachment(picked_movie["images"][0]["remoteUrl"]) if has_poster else None + + color = self._poster_color(picked_movie["images"][0]["remoteUrl"]) if has_poster else "#ff0000" + + description += picked_movie["overview"] + embed = Embed( + picked_movie["title"], + description, + color, + thumbnail=poster, + footer=year + ) + await msg.edit(content="",embed=embed) + + async def show(self, ctx: SlashContext): + msg = await ctx.send("Finding a random show...") + shows = self.sonarr.shows() + if len(shows) == 0: + await msg.edit(content="Unable to find any shows") + return + + picked_show = choice(shows) + description = "" + + if "imdb" in picked_show['ratings']: + description += f"<:imdb:1301506320603676782> {picked_show['ratings']['imdb']['value']}" + description += "\u1CBC\u1CBC" + + if "rottenTomatoes" in picked_show['ratings']: + rt_value = picked_show['ratings']['rottenTomatoes']['value'] + rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>" + description += f"{rt_icon} {rt_value}%" + + if description != "": + description += "\n\n" + + year = EmbedFooter(str(picked_show["year"])) if "year" in picked_show else None + images = {i["coverType"]:i for i in picked_show["images"]} + has_poster = "poster" in images and "remoteUrl" in images["poster"] + poster = EmbedAttachment(images["poster"]["remoteUrl"]) if has_poster else None + + color = self._poster_color(images["poster"]["remoteUrl"]) if has_poster else "#ff0000" + + description += picked_show["overview"] + embed = Embed( + picked_show["title"], + description, + color, + thumbnail=poster, + footer=year + ) + await msg.edit(content="",embed=embed) diff --git a/gwendolyn/funcs/other/roll/ast_nodes.py b/gwendolyn/funcs/other/roll/ast_nodes.py index 2105602..4d43056 100644 --- a/gwendolyn/funcs/other/roll/ast_nodes.py +++ b/gwendolyn/funcs/other/roll/ast_nodes.py @@ -357,8 +357,14 @@ class ComparePoint(Exp): return None if self.comp_op == "=": return 1 if val == r else 0 + if self.comp_op == "<": + return 1 if val < r else 0 if self.comp_op == "<=": return 1 if val <= r else 0 + if self.comp_op == ">": + return 1 if val <= r else 0 + if self.comp_op == ">=": + return 1 if val <= r else 0 else: raise Exception(f"Unknown binop {self.op}") @@ -451,11 +457,12 @@ class Roll(Exp): self.die_type = r2 self.result = [randint(1,r2) for _ in range(r1)] + self.show_list = [str(i) for i in self.result] return self.result def _show(self, vtable): - result = self.eval(vtable) - return [str(i) for i in result] + self.eval(vtable) + return self.show_list @property def die(self) -> int: @@ -505,10 +512,6 @@ class RollKeepHighest(Roll): self.show_list = [str(n) if i in max_indices else f"~~{n}~~" for i, n in enumerate(r1)] return self.result - def _show(self, vtable): - self.eval(vtable) - return self.show_list - def __repr__(self) -> str: return f"kep_highest({self.roll},{self.exp})" @@ -548,10 +551,6 @@ class RollKeepLowest(Roll): self.show_list = [str(n) if i in min_indices else f"~~{n}~~" for i, n in enumerate(r1)] return self.result - def _show(self, vtable): - self.eval(vtable) - return self.show_list - def __repr__(self) -> str: return f"kep_lowest({self.roll},{self.exp})" @@ -591,10 +590,6 @@ class RollMin(Roll): self.result = r1 return self.result - def _show(self, vtable): - self.eval(vtable) - return self.show_list - def __repr__(self) -> str: return f"min({self.roll},{self.exp})" @@ -634,10 +629,6 @@ class RollMax(Roll): self.result = r1 return self.result - def _show(self, vtable): - self.eval(vtable) - return self.show_list - def __repr__(self) -> str: return f"max({self.roll},{self.exp})" @@ -687,9 +678,53 @@ class RollExplode(Roll): return self.result - def _show(self, vtable): - self.eval(vtable) - return self.show_list + def __repr__(self) -> str: + return f"max({self.roll},{self.exp})" + + def __eq__(self, other: Exp) -> bool: + return ( + isinstance(other, RollMax) and + self.roll == other.roll and + self.exp == other.exp + ) + +class RollReroll(Roll): + def __init__(self, roll: Roll, comp: ComparePoint = None, once: bool = False): + self.roll = roll + self.comp = comp + self.once = once + self.result = None + self.show_list = None + + def _eval(self, vtable): + if self.result is not None: + return self.result + + r1 = self.roll.eval(vtable) + + if not (isinstance(r1,list) and all(isinstance(i,int) for i in r1)): + return [] + + d = self.die + + if self.comp is None: + self.comp = ComparePoint("=", ExpInt(1)) + + self.result = [] + self.show_list = [] + + def compare(n, rerolled): + if self.comp.eval(vtable, n) and not (rerolled and self.once): + self.show_list.append(f"~~{n}~~") + compare(randint(1,d), True) + else: + self.result.append(n) + self.show_list.append(str(n)) + + for n in r1: + compare(n, False) + + return self.result def __repr__(self) -> str: return f"max({self.roll},{self.exp})" diff --git a/gwendolyn/funcs/other/roll/lexer.py b/gwendolyn/funcs/other/roll/lexer.py index 46a70a5..1aabd19 100644 --- a/gwendolyn/funcs/other/roll/lexer.py +++ b/gwendolyn/funcs/other/roll/lexer.py @@ -2,18 +2,22 @@ from string import ascii_letters, digits from rply import LexerGenerator -VALID_CHARACTERS = ascii_letters[:3]+ascii_letters[4:]+"_"+digits +VALID_CHARACTERS = ascii_letters+"_"+digits TOKENS = [ ("ROLL_DIE", r"d"), - ("ROLL_KEEP_HIGHEST", r"kh"), ("ROLL_KEEP_LOWEST", r"kl"), + ("ROLL_KEEP_HIGHEST", r"kh?"), + ("ROLL_REROLL_ONCE", r"ro"), + ("ROLL_REROLL", r"r"), ("ROLL_MIN", r"min"), ("ROLL_MAX", r"max"), ("ROLL_EXPLODE", r"!"), ("SYMBOL_LE", r"\<\="), - # ("SYMBOL_GE", r"\>\="), + ("SYMBOL_GE", r"\>\="), + ("SYMBOL_LT", r"\<"), + ("SYMBOL_GT", r"\>"), ("SYMBOL_EQUALS", r"\="), ("SYMBOL_ARROW", r"\-\>"), ("SYMBOL_BACKSLASH", r"\\"), diff --git a/gwendolyn/funcs/other/roll/parser.py b/gwendolyn/funcs/other/roll/parser.py index f02df26..c70879f 100644 --- a/gwendolyn/funcs/other/roll/parser.py +++ b/gwendolyn/funcs/other/roll/parser.py @@ -1,7 +1,7 @@ from rply import ParserGenerator from lexer import TOKENS -from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpMin, ExpMax, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, RollExplode, ComparePoint, ExpIf, ExpTest, ExpApply, ExpLambda, ExpNeg +from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpMin, ExpMax, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, RollExplode, ComparePoint, ExpIf, ExpTest, ExpApply, ExpLambda, ExpNeg, RollReroll class Parser(): @@ -11,11 +11,11 @@ class Parser(): precedence=[ ('left', ["SYMBOL_BACKSLASH","SYMBOL_ARROW"]), ('left', ["KEYWORD_LET", "KEYWORD_IN", "KEYWORD_IF", "KEYWORD_THEN", "KEYWORD_ELSE"]), - ('left', ["SYMBOL_EQUALS", "SYMBOL_LE"]), + ('left', ["SYMBOL_EQUALS", "SYMBOL_LT", "SYMBOL_LE", "SYMBOL_GT", "SYMBOL_GE"]), ('left', ["SYMBOL_PLUS", "SYMBOL_MINUS"]), ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]), ('left', ["ROLL_DIE"]), - ('right', ["ROLL_KEEP_HIGHEST","ROLL_KEEP_LOWEST","ROLL_MIN","ROLL_MAX","ROLL_EXPLODE"]) + ('right', ["ROLL_KEEP_HIGHEST","ROLL_KEEP_LOWEST","ROLL_MIN","ROLL_MAX","ROLL_EXPLODE","ROLL_REROLL","ROLL_REROLL_ONCE"]) ] ) self._get_parser() @@ -97,6 +97,22 @@ class Parser(): def roll_explode_comp(tokens): return RollExplode(tokens[0], tokens[2]) + @self.pg.production('roll : roll ROLL_REROLL ') + def roll_reroll(tokens): + return RollReroll(tokens[0]) + + @self.pg.production('roll : roll ROLL_REROLL comp') + def roll_reroll_comp(tokens): + return RollReroll(tokens[0], tokens[2]) + + @self.pg.production('roll : roll ROLL_REROLL_ONCE') + def roll_reroll_once(tokens): + return RollReroll(tokens[0], None, True) + + @self.pg.production('roll : roll ROLL_REROLL_ONCE comp') + def roll_reroll_once_comp(tokens): + return RollReroll(tokens[0], tokens[2], True) + @self.pg.production('roll : atom ROLL_DIE atom') def roll(tokens): return Roll(tokens[0], tokens[2]) @@ -108,7 +124,10 @@ class Parser(): # Compare Points @self.pg.production("comp : SYMBOL_EQUALS atom") + @self.pg.production("comp : SYMBOL_LT atom") @self.pg.production("comp : SYMBOL_LE atom") + @self.pg.production("comp : SYMBOL_GT atom") + @self.pg.production("comp : SYMBOL_GE atom") def comp_point(tokens): return ComparePoint(tokens[0].value,tokens[1]) diff --git a/gwendolyn/gwendolyn_client.py b/gwendolyn/gwendolyn_client.py index 43d078b..15f7be2 100644 --- a/gwendolyn/gwendolyn_client.py +++ b/gwendolyn/gwendolyn_client.py @@ -5,8 +5,8 @@ from interactions import Client, Status from pymongo import MongoClient # Used for database management from gwendolyn.utils import log -from gwendolyn.exceptions import NoToken, MongoCannotConnect -from gwendolyn.funcs import Other +from gwendolyn.exceptions import NoToken, CannotConnectToService +from gwendolyn.funcs import Other, BetterNetflix, Sonarr, Radarr, TMDb class Gwendolyn(Client): def __init__(self, testing: bool = True): @@ -41,17 +41,25 @@ class Gwendolyn(Client): try: database_client.admin.command("ping") + self.log("Connected to Mango Client") except: - raise MongoCannotConnect() + raise CannotConnectToService("Mango Client") if self.testing: self.log("Testing mode") self.database = database_client["Gwendolyn-Test"] + self.load_extension("interactions.ext.jurigged") else: self.database = database_client["Gwendolyn"] def _add_functions(self): self.other = Other(self) + self.better_netflix = BetterNetflix( + Radarr(getenv("RADARR_IP"),getenv("RADARR_PORT"),getenv("RADARR_API_KEY")), + Sonarr(getenv("SONARR_IP"),getenv("SONARR_PORT"),getenv("SONARR_API_KEY")), + TMDb(getenv("TMDB_API_ACCESS_TOKEN")), + self + ) def _add_extensions(self): """Load cogs.""" diff --git a/gwendolyn/resources/slash_parameters.json b/gwendolyn/resources/slash_parameters.json index 5f0082e..60cf99c 100644 --- a/gwendolyn/resources/slash_parameters.json +++ b/gwendolyn/resources/slash_parameters.json @@ -16,6 +16,18 @@ } ] }, + "echo" : { + "name": "echo", + "description": "Make Gwendolyn repeat something", + "options" : [ + { + "name" : "text", + "description" : "The text you want Gwendolyn to repeat", + "type" : 3, + "required" : "true" + } + ] + }, "gen_name" : { "name": "gen_name", "description": "Generate a random name" @@ -44,5 +56,15 @@ "name" : "thank", "description" : "Thank Gwendolyn for her service" } + }, + "better_netflix": { + "movie": { + "name": "movie", + "description": "Get a random movie from Better Netflix" + }, + "show": { + "name": "show", + "description": "Get a random show from Better Netflix" + } } } \ No newline at end of file diff --git a/gwendolyn/resources/temp.jpg b/gwendolyn/resources/temp.jpg new file mode 100644 index 0000000..f30c886 Binary files /dev/null and b/gwendolyn/resources/temp.jpg differ