Exemplo n.º 1
0
    async def ping(self, ctx: commands.context.Context):
        """
        Tests the bot and Discord message response times
        """
        self._log.debug("Pong!")

        # Time between when the message was sent and when we processed it
        now = datetime.utcnow().timestamp()
        response_time = round(
            (now - ctx.message.created_at.timestamp()) * 1000, 1)

        embed = discord.Embed()
        embed.description = lang('Misc', 'ping_response', {
            'server': response_time,
            'message': 'Pending...'
        })

        # Time between when we sent the message and when it was registered by Discord
        now = datetime.utcnow().timestamp()
        message = await ctx.send(embed=embed)
        message_delay = round((datetime.utcnow().timestamp() - now) * 1000, 1)

        embed.description = lang('Misc', 'ping_response', {
            'server': response_time,
            'message': message_delay
        })
        await message.edit(embed=embed)
Exemplo 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
Exemplo n.º 3
0
    async def info(self, ctx: commands.Context):
        """
        Learn more about the SauceBot project and how to contribute!
        """
        embed = discord.Embed()
        embed.set_thumbnail(url=bot.user.avatar_url)
        embed.title = lang('Misc', 'info_title')
        embed.url = 'https://www.patreon.com/saucebot'
        embed.description = lang('Misc', 'info_desc')

        await ctx.send(embed=embed)
Exemplo n.º 4
0
    async def unabn_guild(self, ctx: commands.context.Context, guild_id: int):
        """
        Removed a specified guild from the bots banlist
        """
        # Make sure the guild has actually been banned
        if not GuildBanlist.check(guild_id):
            await ctx.send(lang('Admin', 'gban_not_banned'), delete_after=15.0)
            return

        self._log.warning(f"Removing guild {guild_id} from the guild banlist")
        GuildBanlist.unban(guild_id)

        await ctx.send(lang('Admin', 'gban_unban_success'))
Exemplo n.º 5
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')))
Exemplo n.º 6
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)
Exemplo n.º 7
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)
Exemplo n.º 8
0
    async def _index_prompt(self, ctx: commands.context.Context,
                            channel: discord.TextChannel, items: list):
        prompt = await channel.send(lang('Sauce', 'multiple_images')
                                    )  # type: discord.Message
        index_range = range(1, min(len(items), 10) + 1)

        # Add the numerical emojis. The syntax is weird for this.
        for index in index_range:
            await prompt.add_reaction(keycap_emoji(index))

        try:
            check = reaction_check(prompt, [ctx.message.author.id],
                                   [keycap_emoji(i) for i in index_range])
            reaction, user = await ctx.bot.wait_for('reaction_add',
                                                    timeout=60.0,
                                                    check=check)
        except asyncio.TimeoutError:
            await ctx.message.delete()
            await prompt.delete()
            return

        await prompt.delete()
        return items[keycap_to_int(reaction) - 1]
Exemplo n.º 9
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()
Exemplo n.º 10
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
Exemplo n.º 11
0
    async def ban_guild(self,
                        ctx: commands.context.Context,
                        guild_id: int,
                        *,
                        reason: typing.Optional[str] = None):
        """
        Bans a specified guild from using SauceBot
        Upon issuing this command, the bot will immediately leave the guild if they are already invited.
        Any future invite requests will be refused.
        """
        # Make sure the guild exists
        guild = ctx.bot.get_guild(guild_id)  # type: discord.Guild
        if not guild:
            await ctx.send(lang('Admin', 'guild_404'))

        # Make sure it's not already banned
        if GuildBanlist.check(guild):
            await ctx.send(lang('Admin', 'gban_already_banned'),
                           delete_after=15.0)
            return

        # Confirm we really want to do this
        confirm_message = await ctx.send(
            lang('Admin', 'gban_confirm', {'guild_name': guild.name}))
        await confirm_message.add_reaction(self.CONFIRM_EMOJI)
        await confirm_message.add_reaction(self.ABORT_EMOJI)

        def _check(_reaction, _user):
            return _user == ctx.message.author and str(
                _reaction.emoji) in [self.CONFIRM_EMOJI, self.ABORT_EMOJI]

        try:
            reaction, user = await ctx.bot.wait_for('reaction_add',
                                                    timeout=60.0,
                                                    check=_check)
        except asyncio.TimeoutError:
            return
        else:
            if str(reaction) != self.CONFIRM_EMOJI:
                return
        finally:
            await confirm_message.delete()

        GuildBanlist.ban(guild, reason)

        # Send the guild owner a ban message
        try:
            await guild.owner.send(
                lang('Admin', 'gban_notice', {'guild_name': guild.name}))
            if reason:
                await guild.owner.send(
                    lang('Admin', 'gban_reason', {'reason': reason}))
        except (discord.Forbidden, AttributeError):
            self._log.warning(
                f"Failed to send ban notice for guild {guild.name} to user {guild.owner}"
            )

        # Leave the guild
        await ctx.send(f'Leaving guild {guild.name} ({guild.id})',
                       delete_after=15.0)
        await guild.leave()