Esempio n. 1
0
    async def apikey(self, ctx: commands.context.Context,
                     api_key: str) -> None:
        """
        Define your own enhanced SauceNao API key for this server.

        This can only be used to add enhanced / upgraded API keys, not freely registered ones. Adding your own enhanced
        API key will remove the shared daily API query limit from your server.

        You can get an enhanced API key from the following page:
        https://saucenao.com/user.php?page=account-upgrades
        """
        await ctx.message.delete()

        # Make sure the API key is formatted properly
        if not self._re_api_key.match(api_key):
            await ctx.send(
                embed=basic_embed(title=lang('Global', 'generic_error'),
                                  description=lang('Sauce', 'bad_api_key')))
            return

        # Test and make sure it's a valid enhanced-level API key
        saucenao = SauceNao(api_key=api_key)
        test = await saucenao.test()

        # Make sure the test went through successfully
        if not test.success:
            self._log.error(
                f"[{ctx.guild.name}] An unknown error occurred while assigning an API key to this server",
                exc_info=test.error)
            await ctx.send(
                embed=basic_embed(title=lang('Global', 'generic_error'),
                                  description=lang('Sauce', 'api_offline')))
            return

        # Make sure this is an enhanced API key
        if test.account_type != ACCOUNT_ENHANCED:
            self._log.info(
                f"[{ctx.guild.name}] Rejecting an attempt to register a free API key"
            )
            await ctx.send(
                embed=basic_embed(title=lang('Global', 'generic_error'),
                                  description=lang('Sauce', 'api_free')))
            return

        Servers.register(ctx.guild, api_key)
        await ctx.send(
            embed=basic_embed(title=lang('Global', 'generic_success'),
                              description=lang('Sauce', 'registered_api_key')))
Esempio n. 2
0
    async def sauce_error(self, ctx: commands.context.Context, error) -> None:
        """
        Override guild cooldowns for servers with their own API keys provided
        Args:
            ctx (commands.context.Context):
            error (Exception):

        Returns:
            None
        """
        if isinstance(error, commands.CommandOnCooldown):
            if Servers.lookup_guild(ctx.guild):
                self._log.info(
                    f"[{ctx.guild.name}] Guild has an enhanced API key; ignoring triggered guild API limit"
                )
                await ctx.reinvoke()
                return

            self._log.info(
                f"[{ctx.guild.name}] Guild has exceeded their available API queries for the day"
            )
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'api_limit_exceeded')))

        raise error
Esempio n. 3
0
 async def stats(self, ctx: commands.Context):
     """
     Displays how many guilds SauceBot is in among statistics
     """
     embed = basic_embed(title=lang('Misc', 'stats_title'))
     embed.add_field(
         name=lang('Misc', 'stats_guilds'),
         value=lang('Misc', 'stats_guilds_desc', {'count': f'{self.get_stat("guild_count"):,}'}),
         inline=True
     )
     embed.add_field(
         name=lang('Misc', 'stats_users'),
         value=lang('Misc', 'stats_users_desc', {'count': f'{self.get_stat("user_count"):,}'}),
         inline=True
     )
     embed.add_field(
         name=lang('Misc', 'stats_queries'),
         value=lang('Misc', 'stats_queries_desc', {'count': f'{self.get_stat("query_count"):,}'}),
         inline=False
     )
     await ctx.reply(embed=embed)
Esempio n. 4
0
    async def query_guild(self, ctx: commands.Context, guild_id: int):
        """
        Queries basic metadata (name, member count) from a guild ID for analytics
        This is boilerplate testing code. In the future we may implement a restriction on how many guilds a single
        user can invite the bot too. This is to prevent users from exploiting the bot via self-botting scripts.
        """
        guild = ctx.bot.get_guild(guild_id)  # type: discord.Guild
        if not guild:
            await ctx.reply(lang('Admin', 'guild_404'))
            return
        owner = guild.owner  # type: typing.Optional[discord.Member]

        embed = basic_embed(title=guild.name)
        embed.add_field(name=lang('Misc', 'stats_guild_id'),
                        value=str(guild.id),
                        inline=True)
        if owner:
            embed.add_field(name=lang('Misc', 'stats_owner_id'),
                            value=str(owner.id),
                            inline=True)
            embed.set_author(name=owner.display_name,
                             icon_url=owner.avatar_url)

        await ctx.reply(embed=embed)
Esempio n. 5
0
    async def sauce(self,
                    ctx: commands.context.Context,
                    url: typing.Optional[str] = None) -> None:
        """
        Get the source of the attached image, the image in the message you replied to, the specified image URL,
        or the last image uploaded to the channel if none of these are supplied
        """
        # No URL specified? Check for attachments.
        image_in_command = bool(url) or bool(
            self._get_image_attachments(ctx.message))

        # Next, check and see if we're replying to a message
        if ctx.message.reference and not image_in_command:
            reference = ctx.message.reference.resolved
            self._log.debug(f"Message reference in command: {reference}")
            if isinstance(reference, discord.Message):
                image_attachments = self._get_image_attachments(reference)
                if image_attachments:
                    if len(image_attachments) > 1:
                        attachment = await self._index_prompt(
                            ctx, ctx.channel, image_attachments)
                        url = attachment.url
                    else:
                        url = image_attachments[0].url

            # If we passed a reference and found nothing, we should abort now
            if not url:
                await ctx.send(
                    embed=basic_embed(title=lang('Global', 'generic_error'),
                                      description=lang('Sauce', 'no_images')))
                return

        # Lastly, if all else fails, search for the last message in the channel with an image upload
        url = url or await self._get_last_image_post(ctx)

        # Still nothing? We tried everything we could, exit with an error
        if not url:
            await ctx.send(
                embed=basic_embed(title=lang('Global', 'generic_error'),
                                  description=lang('Sauce', 'no_images')))
            return

        self._log.info(
            f"[{ctx.guild.name}] Looking up image source/sauce: {url}")

        # Make sure the URL is valid
        if not validate_url(url):
            await ctx.send(
                embed=basic_embed(title=lang('Global', 'generic_error'),
                                  description=lang('Sauce', 'bad_url')))
            return

        # Make sure this user hasn't exceeded their API limits
        if self._check_member_limited(ctx):
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'member_api_limit_exceeded')))
            return

        # Attempt to find the source of this image
        try:
            preview = None
            sauce = await self._get_sauce(ctx, url)
        except (ShortLimitReachedException, DailyLimitReachedException):
            await ctx.message.delete()
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'api_limit_exceeded')),
                           delete_after=30.0)
            return
        except InvalidOrWrongApiKeyException:
            self._log.warning(
                f"[{ctx.guild.name}] API key was rejected by SauceNao")
            await ctx.message.delete()
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'rejected_api_key')),
                           delete_after=30.0)
            return
        except InvalidImageException:
            self._log.info(
                f"[{ctx.guild.name}] An invalid image / image link was provided"
            )
            await ctx.message.delete()
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'no_images')),
                           delete_after=30.0)
            return
        except SauceNaoException:
            self._log.exception(
                f"[{ctx.guild.name}] An unknown error occurred while looking up this image"
            )
            await ctx.message.delete()
            await ctx.send(embed=basic_embed(
                title=lang('Global', 'generic_error'),
                description=lang('Sauce', 'api_offline')),
                           delete_after=30.0)
            return

        # If it's an anime, see if we can find a preview clip
        if isinstance(sauce, AnimeSource):
            preview_file, nsfw = await self._video_preview(sauce, url, True)
            if preview_file:
                if nsfw and not ctx.channel.is_nsfw():
                    self._log.info(
                        f"Channel #{ctx.channel.name} is not NSFW; not uploading an NSFW video here"
                    )
                else:
                    preview = discord.File(
                        BytesIO(preview_file),
                        filename=f"{sauce.title}_preview.mp4".lower().replace(
                            ' ', '_'))

        # We didn't find anything, provide some suggestions for manual investigation
        if not sauce:
            self._log.info(f"[{ctx.guild.name}] No image sources found")
            embed = basic_embed(title=lang('Sauce',
                                           'not_found',
                                           member=ctx.author),
                                description=lang('Sauce', 'not_found_advice'))

            google_url = f"https://www.google.com/searchbyimage?image_url={url}&safe=off"
            ascii_url = f"https://ascii2d.net/search/url/{url}"
            yandex_url = f"https://yandex.com/images/search?url={url}&rpt=imageview"

            urls = [(lang('Sauce', 'google'), google_url),
                    (lang('Sauce', 'ascii2d'), ascii_url),
                    (lang('Sauce', 'yandex'), yandex_url)]
            urls = ' • '.join([f"[{t}]({u})" for t, u in urls])

            embed.add_field(name=lang('Sauce', 'search_engines'), value=urls)
            await ctx.send(embed=embed, delete_after=60.0)
            return

        await ctx.send(embed=await self._build_sauce_embed(ctx, sauce),
                       file=preview)

        # Only delete the command message if it doesn't contain the image we just looked up
        if not image_in_command and not ctx.message.reference:
            await ctx.message.delete()
Esempio n. 6
0
    async def _build_sauce_embed(self, ctx: commands.context.Context,
                                 sauce: GenericSource) -> discord.Embed:
        """
        Builds a Discord embed for the provided SauceNao lookup
        Args:
            ctx (commands.context.Context)
            sauce (GenericSource):

        Returns:
            discord.Embed
        """
        embed = basic_embed()
        embed.set_footer(text=lang('Sauce', 'found', member=ctx.author),
                         icon_url='https://i.imgur.com/Mw109wP.png')
        embed.title = sauce.title or sauce.author_name or "Untitled"
        embed.url = sauce.url
        embed.description = lang('Sauce', 'match_title', {
            'index': sauce.index,
            'similarity': sauce.similarity
        })

        if sauce.author_name and sauce.title:
            embed.set_author(name=sauce.author_name,
                             url=sauce.author_url or EmptyEmbed)
        embed.set_thumbnail(url=sauce.thumbnail)

        if isinstance(sauce, VideoSource):
            embed.add_field(name=lang('Sauce', 'episode'), value=sauce.episode)
            embed.add_field(name=lang('Sauce', 'timestamp'),
                            value=sauce.timestamp)

        if isinstance(sauce, AnimeSource):
            await sauce.load_ids()
            urls = [(lang('Sauce', 'anidb'), sauce.anidb_url)]

            if sauce.mal_url:
                urls.append((lang('Sauce', 'mal'), sauce.mal_url))

            if sauce.anilist_url:
                urls.append((lang('Sauce', 'anilist'), sauce.anilist_url))

            urls = ' • '.join([f"[{t}]({u})" for t, u in urls])
            embed.add_field(name=lang('Sauce', 'more_info'),
                            value=urls,
                            inline=False)

        if isinstance(sauce, MangaSource):
            embed.add_field(name=lang('Sauce', 'chapter'), value=sauce.chapter)

        if isinstance(sauce, BooruSource):
            if sauce.characters:
                characters = [c.title() for c in sauce.characters]
                embed.add_field(name=lang('Sauce', 'characters'),
                                value=', '.join(characters),
                                inline=False)
            if sauce.material:
                material = [m.title() for m in sauce.material]
                embed.add_field(name=lang('Sauce', 'material'),
                                value=', '.join(material),
                                inline=False)

        return embed