diff --git a/cogs/events.py b/cogs/events.py index bf6c31ca..1ce8e303 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -9,7 +9,7 @@ import discord from discord.ext import commands, tasks from loguru import logger -from modules.media_embedders import InstagramEmbedder, TikTokEmbedder +from modules.media_embedders import BaseEmbedder, InstagramEmbedder, TikTokEmbedder from modules.misobot import MisoBot from modules import emoji_literals, queries, util @@ -331,11 +331,27 @@ async def get_autoembed_options( guild_id, provider, ) - print(options_data) if options_data: return options_data - return None, None + return None, True + + async def embed_posts( + self, posts: list, message: discord.Message, embedder: BaseEmbedder + ): + options, should_reply = await self.get_autoembed_options( + message.guild.id, embedder.NAME + ) + embed_options = embedder.get_options(options) if options else None + for post in posts: + async with message.channel.typing(): + if should_reply: + await embedder.send_reply(message, post, embed_options) + else: + await embedder.send_contextless( + message.channel, message.author, post, embed_options + ) + await util.suppress(message) async def parse_media_auto_embed( self, message: discord.Message, media_settings: dict @@ -344,42 +360,13 @@ async def parse_media_auto_embed( embedder = InstagramEmbedder(self.bot) posts = embedder.extract_links(message.content, include_shortcodes=False) if posts: - options, should_reply = await self.get_autoembed_options( - message.guild.id, "instagram" - ) - for post in posts: - async with message.channel.typing(): - if not should_reply: - await embedder.send_contextless( - message.channel, - message.author, - post, - embedder.get_options(options) if options else None, - ) - else: - await embedder.send_reply(message, post) - await util.suppress(message) + await self.embed_posts(posts, message, embedder) if media_settings["tiktok"]: embedder = TikTokEmbedder(self.bot) - links = embedder.extract_links(message.content) - if links: - options, should_reply = await self.get_autoembed_options( - message.guild.id, - "tiktok", - ) - for link in links: - async with message.channel.typing(): - if not should_reply: - await embedder.send_contextless( - message.channel, - message.author, - link, - embedder.get_options(options) if options else None, - ) - else: - await embedder.send_reply(message, link) - await util.suppress(message) + posts = embedder.extract_links(message.content) + if posts: + await self.embed_posts(posts, message, embedder) @staticmethod async def easter_eggs(message: discord.Message): diff --git a/cogs/fishy.py b/cogs/fishy.py index dcdcae82..c8554aa7 100644 --- a/cogs/fishy.py +++ b/cogs/fishy.py @@ -325,6 +325,104 @@ async def trash(self, ctx: commands.Context, user, gift): ) return 0 + @commands.command(hidden=True) + @commands.is_owner() + async def fishytransfer( + self, + ctx: commands.Context, + user_from: int, + user_to: int, + ): + """Transfer fishing data from one account to another""" + data = await self.bot.db.fetch_row( + """ + SELECT fishy_count, fishy_gifted_count, biggest_fish, + trash, common, uncommon, rare, legendary + FROM fishy JOIN fish_type + ON fishy.user_id = fish_type.user_id + WHERE fishy.user_id = %s + """, + user_from, + ) + + olddata = await self.bot.db.fetch_row( + """ + SELECT fishy_count, fishy_gifted_count, biggest_fish, + trash, common, uncommon, rare, legendary + FROM fishy JOIN fish_type + ON fishy.user_id = fish_type.user_id + WHERE fishy.user_id = %s + """, + user_to, + ) + + if not data: + return await ctx.send(f"User {user_from} has no data!") + + await self.bot.db.execute( + """ + INSERT INTO fishy (user_id, fishy_count, fishy_gifted_count, biggest_fish) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + fishy_count = fishy_count + VALUES(fishy_count), + fishy_gifted_count = fishy_gifted_count + VALUES(fishy_gifted_count), + biggest_fish = GREATEST(biggest_fish, VALUES(biggest_fish)) + """, + user_to, + data[0], + data[1], + data[2], + ) + for catch, amount in [ + ("trash", data[3]), + ("common", data[4]), + ("uncommon", data[5]), + ("rare", data[6]), + ("legendary", data[7]), + ]: + await self.bot.db.execute( + f""" + INSERT INTO fish_type (user_id, {catch}) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE + {catch} = {catch} + VALUES({catch}) + """, + user_to, + amount, + ) + + newdata = await self.bot.db.fetch_row( + """ + SELECT fishy_count, fishy_gifted_count, biggest_fish, + trash, common, uncommon, rare, legendary + FROM fishy JOIN fish_type + ON fishy.user_id = fish_type.user_id + WHERE fishy.user_id = %s + """, + user_to, + ) + + await self.bot.db.execute( + """ + DELETE FROM fishy WHERE user_id = %s + """, + user_from, + ) + + await self.bot.db.execute( + """ + DELETE FROM fish_type WHERE user_id = %s + """, + user_from, + ) + + await ctx.send( + f"{user_from} = `{data}`\n" + f"{user_to} = `{olddata}`\n" + "- moving data...\n- data is now:\n" + f"{user_to} = `{newdata}`\n" + ) + async def setup(bot): await bot.add_cog(Fishy(bot)) diff --git a/cogs/lastfm.py b/cogs/lastfm.py index ada6c3e7..f108665e 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -751,6 +751,7 @@ async def artist_overview(self, ctx: MisoContext, timeframe: Period, artist: str """, ctx.guild.id, formatted_name, + ) if crown_holder_id == ctx.lfm.target_user.id: crownstate = ":crown: " diff --git a/cogs/media.py b/cogs/media.py index a59f01f5..11d4a967 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -190,6 +190,7 @@ async def autoembedder_reply(self, ctx: commands.Context, on_or_off: bool): aliases=["ig", "insta"], usage="[OPTIONS] ", ) + @util.patrons_only() async def instagram(self, ctx: commands.Context, *, links: str): """Retrieve media from Instagram post, reel or story diff --git a/cogs/misc.py b/cogs/misc.py index 4452e854..af4baa16 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -840,7 +840,7 @@ async def meme(self, ctx: commands.Context, template: str, *, content): """Make memes with given templates of empty signs Available templates: - olivia, yyxy, haseul, jihyo, dubu, chaeyoung, nayeon, trump + olivia, yyxy, haseul, jihyo, dubu, chaeyoung, nayeon, trump, yeji """ options = {} @@ -899,6 +899,13 @@ async def meme(self, ctx: commands.Context, template: str, *, content): "wm_color": (255, 255, 255, 255), "angle": 5, } + case "yeji": + options = { + "filename": "images/yeji.jpg", + "boxdimensions": (139, 555, 529, 350), + "wm_color": (255, 255, 255, 255), + "angle": 0, + } case _: raise exceptions.CommandWarning(f"No meme template called `{template}`") diff --git a/cogs/utility.py b/cogs/utility.py index 551d8061..a841f1a3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -3,6 +3,7 @@ # https://git.joinemm.dev/miso-bot import html +import io import json import random from time import time @@ -1124,6 +1125,33 @@ async def market(self, ctx: commands.Context, *, search_term: str): await MarketPaginator(data["results"]).run(ctx) + @commands.command() + async def graph(self, ctx: commands.Context, *, code: str): + """Generate a graph from code using Mermaid language + + Syntax reference: https://mermaid.js.org/intro/syntax-reference.html + """ + async with self.bot.session.post( + "https://kroki.io/", + json={ + "diagram_source": code.strip("`"), + "diagram_type": "mermaid", + "output_format": "png", + "diagram_options": {"theme": "dark"}, + }, + ) as response: + if not response.ok: + error = await response.text() + raise exceptions.CommandError(error.split(" at", 1)[0]) + + buffer = io.BytesIO(await response.read()) + await ctx.send( + file=discord.File( + fp=buffer, + filename="miso_bot_mermaid_graph.png", + ), + ) + async def setup(bot): await bot.add_cog(Utility(bot)) diff --git a/dev-requirements.txt b/dev-requirements.txt index 7e66a714..df650a03 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,13 +4,13 @@ # # pip-compile --resolver=backtracking dev-requirements.in # -astroid==2.15.6 +astroid==3.0.1 # via pylint -black==23.7.0 +black==23.11.0 # via -r dev-requirements.in -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -click==8.1.6 +click==8.1.7 # via # -c requirements.txt # black @@ -18,16 +18,14 @@ dill==0.3.7 # via pylint distlib==0.3.7 # via virtualenv -filelock==3.12.2 +filelock==3.13.1 # via virtualenv -identify==2.5.26 +identify==2.5.31 # via pre-commit isort==5.12.0 # via # -r dev-requirements.in # pylint -lazy-object-proxy==1.9.0 - # via astroid mccabe==0.7.0 # via pylint mypy-extensions==1.0.0 @@ -36,35 +34,31 @@ nodeenv==1.8.0 # via # pre-commit # pyright -packaging==23.1 +packaging==23.2 # via # -c requirements.txt # black -pathspec==0.11.1 +pathspec==0.11.2 # via black -platformdirs==3.9.1 +platformdirs==3.11.0 # via # black # pylint # virtualenv -pre-commit==3.3.3 +pre-commit==3.5.0 # via -r dev-requirements.in -pylint==2.17.4 +pylint==3.0.2 # via -r dev-requirements.in -pyright==1.1.318 +pyright==1.1.336 # via -r dev-requirements.in pyyaml==6.0.1 # via pre-commit -ruff==0.0.280 +ruff==0.1.6 # via -r dev-requirements.in -tomlkit==0.11.8 +tomlkit==0.12.3 # via pylint -virtualenv==20.24.1 +virtualenv==20.24.6 # via pre-commit -wrapt==1.15.0 - # via - # -c requirements.txt - # astroid # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/images/yeji.jpg b/images/yeji.jpg new file mode 100644 index 00000000..8abbe1c9 Binary files /dev/null and b/images/yeji.jpg differ diff --git a/modules/instagram.py b/modules/instagram.py index 7b57dfe3..2835a282 100644 --- a/modules/instagram.py +++ b/modules/instagram.py @@ -14,8 +14,6 @@ import redis from loguru import logger -from modules import exceptions - if TYPE_CHECKING: from modules.misobot import MisoBot @@ -94,7 +92,7 @@ def decode(shortcode, alphabet=ENCODING_CHARS): class Datalama: - BASE_URL = "https://api.datalama.io" + BASE_URL = "https://api.datalikers.com" def __init__(self, bot: "MisoBot"): self.bot: "MisoBot" = bot @@ -146,9 +144,6 @@ async def save_cache(self, cache_key: str, data: dict, lifetime: int): logger.warning("Could not save content into redis cache (ConnectionError)") async def api_request(self, endpoint: str, params: dict) -> dict: - raise exceptions.CommandWarning( - "The Instagram scraper was taken down by Meta Inc :(" - ) headers = { "accept": "application/json", "x-access-key": self.bot.keychain.DATALAMA_ACCESS_KEY, diff --git a/modules/media_embedders.py b/modules/media_embedders.py index bd01aafc..f490b778 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -52,6 +52,8 @@ def filesize_limit(guild: discord.Guild | None): class BaseEmbedder: NO_RESULTS_ERROR = "..." + NAME = "..." + EMOJI = "..." def __init__(self, bot) -> None: self.bot: MisoBot = bot @@ -88,7 +90,10 @@ async def process(self, ctx: commands.Context, user_input: str): await self.send(ctx, result, options=options) if options.delete_after: - await ctx.message.delete() + try: + await ctx.message.delete() + except discord.errors.NotFound: + pass else: await util.suppress(ctx.message) @@ -157,13 +162,16 @@ async def download_media( return media_url async def send( - self, ctx: commands.Context, media: Any, options: Options | None = None + self, + ctx: commands.Context, + media: Any, + options: Options | None = None, ): """Send the media to given context""" message_contents = await self.create_message( ctx.channel, media, options=options ) - msg = await ctx.send(**message_contents) + msg = await ctx.send(**message_contents, suppress_embeds=True) message_contents["view"].message_ref = msg message_contents["view"].approved_deletors.append(ctx.author) @@ -176,23 +184,29 @@ async def send_contextless( ): """Send the media without relying on command context, for example in a message event""" message_contents = await self.create_message(channel, media, options=options) - msg = await channel.send(**message_contents) + msg = await channel.send(**message_contents, suppress_embeds=True) message_contents["view"].message_ref = msg message_contents["view"].approved_deletors.append(author) async def send_reply( - self, message: discord.Message, media: Any, options: Options | None = None + self, + message: discord.Message, + media: Any, + options: Options | None = None, ): """Send the media as a reply to another message""" message_contents = await self.create_message( message.channel, media, options=options ) - msg = await message.reply(**message_contents, mention_author=False) + msg = await message.reply( + **message_contents, mention_author=False, suppress_embeds=True + ) message_contents["view"].message_ref = msg message_contents["view"].approved_deletors.append(message.author) class InstagramEmbedder(BaseEmbedder): + NAME = "instagram" EMOJI = "<:ig:937425165162262528>" NO_RESULTS_ERROR = "Found no Instagram links to embed!" @@ -281,6 +295,7 @@ async def create_message( class TikTokEmbedder(BaseEmbedder): + NAME = "tiktok" EMOJI = "<:tiktok:1050401570090647582>" NO_RESULTS_ERROR = "Found no TikTok links to embed!" @@ -344,6 +359,7 @@ async def create_message( class TwitterEmbedder(BaseEmbedder): + NAME = "twitter" EMOJI = "<:x_:1135484782642466897>" NO_RESULTS_ERROR = "Found no Twitter/X links to embed!" @@ -397,7 +413,7 @@ async def create_message( if not tweet["media_extended"]: raise exceptions.CommandWarning( - f"Tweet `{tweet['url']}` does not include any media.", + f"Tweet with id `{tweet_id}` does not contain any media!", ) for media in tweet["media_extended"]: @@ -475,6 +491,6 @@ async def on_timeout(self): self.remove_item(self.delete_button) if self.message_ref: try: - await self.message_ref.edit(view=self) + await self.message_ref.edit(view=self, suppress=True) except discord.NotFound: pass diff --git a/requirements.txt b/requirements.txt index cec3185e..5d83081b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ # # pip-compile --resolver=backtracking requirements.in # -aiodns==3.0.0 +aiodns==3.1.1 # via discord-py aiofiles==22.1.0 # via shazamio -aiohttp==3.8.5 +aiohttp==3.9.0 # via # -r requirements.in # aiohttp-cors @@ -23,53 +23,49 @@ aiosignal==1.3.1 # via aiohttp anyio==3.7.1 # via shazamio -arrow==1.2.3 +arrow==1.3.0 # via -r requirements.in astunparse==1.6.3 # via import-expression async-cse==0.3.0 # via -r requirements.in -async-timeout==4.0.2 - # via aiohttp attrs==23.1.0 # via aiohttp beautifulsoup4==4.12.2 # via # -r requirements.in # markdownify -bleach==6.0.0 +bleach==6.1.0 # via -r requirements.in braceexpand==0.1.7 # via jishaku -brotli==1.0.9 +brotli==1.1.0 # via discord-py -cffi==1.15.1 +cffi==1.16.0 # via pycares -charset-normalizer==3.2.0 - # via aiohttp -click==8.1.6 +click==8.1.7 # via jishaku colorgram-py==1.2.0 # via -r requirements.in -contourpy==1.1.0 +contourpy==1.2.0 # via matplotlib -cycler==0.11.0 +cycler==0.12.1 # via matplotlib dataclass-factory==2.16 # via shazamio -discord-py[speed]==2.3.1 +discord-py[speed]==2.3.2 # via -r requirements.in -dnspython==2.3.0 +dnspython==2.4.2 # via minestat durations-nlp==1.0.1 # via -r requirements.in -fonttools==4.41.1 +fonttools==4.44.3 # via matplotlib frozenlist==1.4.0 # via # aiohttp # aiosignal -humanize==4.7.0 +humanize==4.8.0 # via -r requirements.in idna==3.4 # via @@ -79,67 +75,65 @@ import-expression==1.1.4 # via jishaku iniconfig==2.0.0 # via pytest -jishaku==2.5.1 +jishaku==2.5.2 # via -r requirements.in kdtree==0.16 # via -r requirements.in -kiwisolver==1.4.4 +kiwisolver==1.4.5 # via matplotlib -line-profiler==4.0.3 - # via jishaku -loguru==0.7.0 +loguru==0.7.2 # via -r requirements.in lxml==4.9.3 # via -r requirements.in markdownify==0.11.6 # via -r requirements.in -matplotlib==3.7.2 +matplotlib==3.8.2 # via -r requirements.in -minestat==2.6.1 +minestat==2.6.2 # via -r requirements.in multidict==6.0.4 # via # aiohttp # yarl -numpy==1.25.1 +numpy==1.26.2 # via # -r requirements.in # contourpy # matplotlib # scipy # shazamio -orjson==3.9.2 +orjson==3.9.10 # via discord-py -packaging==23.1 +packaging==23.2 # via # matplotlib # pytest -pillow==10.0.1 +pillow==10.1.0 # via # -r requirements.in # colorgram-py # matplotlib -pluggy==1.2.0 +pluggy==1.3.0 # via pytest prometheus-async==22.2.0 # via -r requirements.in -prometheus-client==0.17.1 +prometheus-client==0.18.0 # via prometheus-async -psutil==5.9.5 +psutil==5.9.6 # via -r requirements.in -pycares==4.3.0 +pycares==4.4.0 # via aiodns pycparser==2.21 # via cffi -pydantic==1.10.11 +pydantic==1.10.13 # via shazamio pydub==0.25.1 # via shazamio pymysql==1.1.0 # via aiomysql -pyparsing==3.0.9 +pyparsing==3.1.1 # via matplotlib -pytest==7.4.0 +pytest==7.4.3 # via # pytest-asyncio # shazamio @@ -153,11 +147,11 @@ python-dotenv==1.0.0 # via -r requirements.in random-user-agent==1.0.1 # via -r requirements.in -redis==4.6.0 +redis==5.0.1 # via -r requirements.in -regex==2023.6.3 +regex==2023.10.3 # via -r requirements.in -scipy==1.11.1 +scipy==1.11.3 # via -r requirements.in shazamio==0.4.0.1 # via -r requirements.in @@ -169,17 +163,19 @@ six==1.16.0 # python-dateutil sniffio==1.3.0 # via anyio -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 -typing-extensions==4.7.1 +types-python-dateutil==2.8.19.14 + # via arrow +typing-extensions==4.8.0 # via pydantic -uvloop==0.17.0 +uvloop==0.19.0 # via -r requirements.in webencodings==0.5.1 # via bleach -wheel==0.41.0 +wheel==0.41.3 # via astunparse -wrapt==1.15.0 +wrapt==1.16.0 # via prometheus-async yarl==1.9.2 # via aiohttp