async def cleanup_users(self, ctx, days: int = 1): """Cleanup inactive server members""" if days > 30: await ctx.send( chat.info( _("Due to Discord Restrictions, you cannot use more than 30 days for that cmd." ))) days = 30 elif days <= 0: await ctx.send(chat.info(_('"days" arg cannot be less than 1...'))) days = 1 to_kick = await ctx.guild.estimate_pruned_members(days=days) await ctx.send( chat.warning( _("You about to kick **{to_kick}** inactive for **{days}** days members from this server. " 'Are you sure?\nTo agree, type "yes"').format( to_kick=to_kick, days=days))) pred = MessagePredicate.yes_or_no(ctx) try: await self.bot.wait_for("message", check=pred, timeout=30) except AsyncTimeoutError: pass if pred.result: cleanup = await ctx.guild.prune_members(days=days, reason=get_audit_reason( ctx.author)) await ctx.send( chat.info( _("**{removed}**/**{all}** inactive members removed.\n" "(They was inactive for **{days}** days)").format( removed=cleanup, all=to_kick, days=days))) else: await ctx.send(chat.error(_("Inactive members cleanup canceled.")))
async def global_api(self, ctx: commands.Context, service: str, api_key: str = None): """Get information on setting an API key for a global service.""" if api_key: # Try deleting the command as fast as possible, so that others can't see the API key await delete(ctx.message) if service in self.supported_guild_services: await ctx.send( info( "{} is not a global service, and should be set up per guild " .format(self.get_nice_service_name(service)) + "using the command:\n\n" + "`[p]bancheckset service api {} <your_api_key_here>`". format(service))) return if service not in self.supported_global_services: await ctx.send( error("{} is not a valid service name.".format( self.get_nice_service_name(service)))) return await ctx.send( info( "Global API keys are no longer set here. You should run this command instead:\n\n" + "`[p]set api {} api_key <your_api_key_here>`".format(service)))
async def request_channel(self, ctx, channel: discord.TextChannel = None): """Where `[p]request list` commands say to use the `[p]request` command. Use the command without a channel argument to set to no channel.""" if channel is None: channel_id = 0 elif channel.guild != ctx.guild: await self._raw_send_no_mention( ctx, error("Channel not found on this server.")) return else: channel_id = channel.id await self.config.guild(ctx.guild).request_channel.set(channel_id) if await self.config.guild(ctx.guild).auto_post_list(): await self._auto_post_list(ctx) if channel_id != 0: await self._raw_send_no_mention( ctx, info( "The `{p}request list` response now suggests using the `{p}request` commands in {channel}." .format(p=ctx.prefix, channel=channel.mention))) else: await self._raw_send_no_mention( ctx, info( "The `{p}request list` response no longer suggests a channel to use `{p}request` commands in." .format(p=ctx.prefix)))
async def list(self, ctx): """List terms from the auto-ban list.""" async with self.config.guild(ctx.guild).terms() as terms: if len(terms) == 0: await ctx.send(info("No terms set on this server.")) else: ls = humanize_list(["`{}`".format(term) for term in terms]) await ctx.send(info("Terms on this server:\n{}".format(ls)))
async def auto_post_list(self, ctx, value: bool = None): """Whether to automatically update existing post_list posts when roles or counts change. For value, pass in "true" or "false". Omit the value to toggle.""" if value is None: value = not await self.config.guild(ctx.guild).auto_post_list() await self.config.guild(ctx.guild).auto_post_list.set(value) if value: await ctx.send(info("Will automatically update post_list posts.")) else: await ctx.send( info("Will not automatically update post_list posts."))
async def request_channel(self, ctx, channel : discord.TextChannel = None): """Where `[p]request list` commands say to use the `[p]request` command.""" if channel is None: channel_id = 0 elif channel.guild != ctx.guild: await ctx.send(error("Channel not found on this server.")) return else: channel_id = channel.id await self.config.guild(ctx.guild).request_channel.set(channel_id) if channel_id != 0: await ctx.send(info("Set where to suggest using the `[p]request` commands in `[p]request list` to {}.".format(channel.mention))) else: await ctx.send(info("Set the `[p]request list` to not suggest a channel."))
async def auto_react_match_text(self, ctx, match_text: str = ""): """Text to check for to automatically react, to allow users to one-click decode.""" if "@" in match_text: await ctx.send(error("You cannot use that match text.")) return await self.config.guild(ctx.guild).auto_react_to.set(match_text) if match_text: await ctx.send( info( "If new user messages on this server match `{}`, I will add a reaction automatically." .format(match_text))) else: await ctx.send( info("Disabled automatically reacting on this erver."))
async def captcha_type_setter(self, ctx: commands.Context, captcha_type: str): """ Change the type of Captcha challenge. You choose the following type of Captcha: - image: A normal captcha image, hardest to understand. - wheezy: A less complex captcha image, easier than image method. - plain: Plain text captcha. Easiest to read, cannot be copy/pasted. """ available = ("wheezy", "image", "plain") captcha_type = captcha_type.lower() if captcha_type not in available: await ctx.send_help() await ctx.send( form.error( form.bold( "{type} is not a valid Captcha type option.".format( type=form.bordered(captcha_type))))) return await self.data.guild(ctx.guild).type.set(captcha_type) await ctx.send( form.info( "Captcha type registered: {type}".format(type=captcha_type)))
async def other(self, ctx: commands.Context): """Learn how to modify default bitrate and user limits.""" await ctx.send( info( "Default bitrate and user limit settings are now copied from the AutoRoom Source." ) )
async def mr_list(self, ctx): """Assigned roles list""" members_data = await self.config.all_members(ctx.guild) if not members_data: await ctx.send( chat.info( _("There is no assigned personal roles on this server"))) return assigned_roles = [] for member, data in members_data.items(): if not data["role"]: continue dic = { _("User"): ctx.guild.get_member(member) or f"[X] {await self.bot.fetch_user(member)}", _("Role"): await self.smart_truncate( ctx.guild.get_role(data["role"]) or "[X] {}".format(data["role"])), } assigned_roles.append(dic) pages = list( chat.pagify( tabulate(assigned_roles, headers="keys", tablefmt="orgtbl"))) pages = [chat.box(page) for page in pages] await menu(ctx, pages, DEFAULT_CONTROLS)
async def cogwhitelist_reset(self, ctx: commands.Context): """Reset whitelisted cog settings""" cogs = len(await self.config.cogs()) if not cogs: return await fmt( ctx, info( _( "No cogs are currently setup to require a whitelist to use, and as such " "you cannot reset any cog whitelist settings." ) ), ) warn_str = warning( _( "Are you sure you want to reset your cog whitelist settings?\n" "This action will make {cogs} cog(s) usable by any server.\n\n" "Unless you have a time machine, **this action cannot be undone.**" ) ) if await ConfirmMenu(ctx=ctx, content=warn_str.format(cogs=cogs)).prompt(): await self.config.cogs.set({}) await ctx.tick() else: await ctx.send(_("Ok then."))
async def logging_channel(self, ctx: commands.Context, *, destination: Union[discord.TextChannel, str]): """Set a channel where events are registered. The following actions will be registered: - User join and must answer the captcha. - User passed captcha/failed captcha. - User got kicked. ``destination``: Text channel. You can also use "None" if you wish to remove the logging channel. """ if not isinstance(destination, discord.TextChannel): if destination.lower() == "none": await self.data.guild(ctx.guild).logschannel.clear() await ctx.send(form.info("Logging channel removed.")) else: await ctx.send(form.error("Invalid destination.")) return if needperm := await check_permissions_in_channel( [ "read_messages", "read_message_history", "send_messages", "attach_files", ], destination, ): await ctx.send( embed=await build_embed_with_missing_permissions(needperm)) return
async def delsfx(self, ctx, soundname: str): """Deletes an existing sound.""" # await self.bot.type() server = ctx.message.guild serverid = str(server.id) if serverid not in os.listdir(self.sound_base): os.makedirs(os.path.join(self.sound_base, serverid)) f = glob.glob(os.path.join(self.sound_base, serverid, soundname + ".*")) if len(f) < 1: await ctx.send(cf.error( "Sound file not found. Try `{}allsfx` for a list.".format( ctx.prefix))) return elif len(f) > 1: await ctx.send(cf.error( "There are {} sound files with the same name, but different" " extensions, and I can't deal with it. Please make filenames" " (excluding extensions) unique.".format(len(f)))) return os.remove(f[0]) if soundname in self.settings[serverid]: del self.settings[serverid][soundname] await self.save_json(self.settings_path, self.settings) await ctx.send(cf.info("Sound {} deleted.".format(soundname)))
async def clear(self, ctx, channel: discord.TextChannel = None): """Clear the channel topic. If the channel is omitted, the current channel is assumed.""" if channel is None: # Did not provide channel; get current channel channel = ctx.channel if channel.guild != ctx.guild: await ctx.send(content=error("I cannot do that.")) return if isinstance(channel, discord.TextChannel): # This not is a DM or group DM # async with ctx.typing(): reason = "Topic edit [clear] by request of {author} ({id})" \ .format(author=ctx.author, id=ctx.author.id) try: await channel.edit(topic="", reason=reason) except (discord.Forbidden, discord.HTTPException): await ctx.send(content=error( "I don't have permission to edit this topic!")) return await ctx.send(content=info( "Channel {channel}'s topic has been cleared.".format( channel=channel.mention))) else: # Private location: only reply with text await ctx.send( content=error("This channel type cannot have a topic."))
async def global_api( self, ctx: commands.Context, service: str, api_key: str = None ): """Set (or delete) an API key for a global service. Behind the scenes, this is the same as `[p]set api <service> api_key <your_api_key_here>` """ if api_key: # Try deleting the command as fast as possible, so that others can't see the API key await delete(ctx.message) if service in self.supported_guild_services: await ctx.send( info( f"{self.get_nice_service_name(service)} is not a global service, " "and should be set up per server using the command:\n\n" f"`[p]bancheckset service api {service} <your_api_key_here>`" ) ) return if service not in self.supported_global_services: await ctx.send( error( f"{self.get_nice_service_name(service)} is not a valid service name." ) ) return action = "set" if api_key: await ctx.bot.set_shared_api_tokens(service, api_key=api_key) else: await ctx.bot.remove_shared_api_tokens(service, "api_key") action = "removed" response = f"API key for the {self.get_nice_service_name(service)} BanCheck service has been {action}." await ctx.send(checkmark(response))
async def disable_autocheck(self, ctx: commands.Context): """Disable automatically checking new users against ban lists.""" if await self.config.guild(ctx.guild).notify_channel() is None: await ctx.send(info("AutoCheck is already disabled.")) else: await self.config.guild(ctx.guild).notify_channel.set(None) await ctx.send(checkmark("AutoCheck is now disabled."))
async def postlist(self, ctx, channel: discord.TextChannel): """Lists the roles that can be requested and posts them permanently to the specified channel.""" msg = await self._get_role_list_message(ctx) if channel is None: channel = ctx.channel if channel.guild.id != ctx.guild.id: await self._raw_send_no_mention( ctx, error("That channel is not on this server.")) return post_id = await self.config.channel(channel).role_info_post() result = await self._post_list(channel, post_id, msg) upd_resp = "Manually update it later with `{p}request postlist #{channel}`." if result is None: resp = "No update needed." elif result: resp = "Message updated." else: resp = "Message posted." await self._raw_send_no_mention( ctx, info("{} {}".format(resp, upd_resp).format(p=ctx.prefix, channel=channel)))
async def mr_list(self, ctx): """Assigned roles list""" members_data = await self.config.all_members(ctx.guild) assigned_roles = [] for member, data in members_data.items(): if not data["role"]: continue dic = { _("User"): ctx.guild.get_member(member) or f"[X] {member}", _("Role"): shorten( str( ctx.guild.get_role(data["role"]) or "[X] {}".format(data["role"])), 32, placeholder="…", ), } assigned_roles.append(dic) pages = list( chat.pagify( tabulate(assigned_roles, headers="keys", tablefmt="orgtbl"))) pages = [chat.box(page) for page in pages] if pages: await menu(ctx, pages, DEFAULT_CONTROLS) else: await ctx.send( chat.info( _("There is no assigned personal roles on this server")))
async def addTag(self, ctx: Context, user: discord.User, *, description: str): """Add a description to a user. Parameters: ----------- user: discord.User The user to add a description to. description: str The description to add. """ userId = str(user.id) if len(description) > MAX_DESCRIPTION_LENGTH: await ctx.send( "The description is too long! " f"Max length is {MAX_DESCRIPTION_LENGTH} characters.") return async with self.config.guild( ctx.guild).get_attr(KEY_DESCRIPTIONS)() as descDict: descDict[userId] = description await ctx.send( info(f"Description set for {user.mention}."), allowed_mentions=discord.AllowedMentions.none(), ) LOGGER.info( "A welcome description has been added for %s#%s (%s)", user.name, user.discriminator, user.id, ) LOGGER.debug(description)
async def getTag(self, ctx: Context, user: discord.User): """Get a description for a user. Parameters: ----------- user: discord.User The user to get a description for. """ userId = str(user.id) descDict: dict = await self.config.guild(ctx.guild ).get_attr(KEY_DESCRIPTIONS)() if userId in descDict: description = descDict[userId] if description: descText = "\n".join( [f"**{user.mention}:**", box(description)]) embed = discord.Embed( title=f"Description for {user.name}#{user.discriminator}", description=descText) await ctx.send(embed=embed, allowed_mentions=discord.AllowedMentions.none()) return await ctx.send(info("No description found for that user."))
async def server(self, ctx: commands.Context): """Ignore/Unignore the current server.""" await ctx.send( info( "Use the `[p]command enablecog` and `[p]command disablecog` to enable or disable this cog." ) )
async def globalstats(self, ctx, *, date : str = None): """ Command to check global Coronavirus stats date : it can be almost any kind of date format for the required date Example : [p]globalstats "3 March 2020" or [p]globalstats 3/3/2020 or [p]globalstats "2020 3 March" """ global_data = await data_client.get_global_stats() response_embed = discord.Embed(title="Global Coronavirus Stats", color=discord.Color.red()) current_day_data = {} if not date: current_day_data = global_data[0] else: try: required_date = date_parser.parse(date) except date_parser.ParserError: return await ctx.send(chat_formatter.error("The specified date is invalid.")) current_day_data = [day_data for day_data in global_data if date_parser.parse(day_data['date']).date() == required_date.date()] if not current_day_data: response_embed.add_field(name="Overall stats", value=chat_formatter.info("No data found for the current day."), inline=True) return await ctx.send(embed=response_embed) response_embed.add_field(name="Overall stats", value="", inline=True) self.add_stats_field(response_embed, 0, current_day_data, "confirmed", "Total cases", "new_confirmed") self.add_stats_field(response_embed, 0, current_day_data, "deaths", "Total deaths", "new_deaths") self.add_stats_field(response_embed, 0, current_day_data, "recovered", "Total recovered", "new_recovered") graph = Graph(ctx.bot, "Global Coronavirus(Sars-Cov2) stats", "Date", "Cases", global_data[::-1], "seaborn") await graph.plot("date", "confirmed", "b", "Confirmed cases") await graph.plot("date", "deaths", "r", "Deaths") await graph.plot("date", "recovered", "g", "Recovered cases") await graph.save() img_file = discord.File(graph.file_obj, filename='globalstats.png') response_embed.set_image(url='attachment://globalstats.png') await ctx.send(embed=response_embed, file=img_file)
async def set(self, ctx, channel: discord.TextChannel, *, topic: str): """Set the channel topic to the given value. Any custom emojis, mentions, links, and other formatting are supported. Note that mentions (including @here and @everyone) in your own message will ping, so best to set the channel from another channel (or the server's settings) if those are needed.""" # if isinstance(channel, str): # # Did not provide channel; get current channel # topic = "{} {}".format(channel, topic) # channel = ctx.channel if channel.guild != ctx.guild: await ctx.send(content=error("I cannot do that.")) return if isinstance(channel, discord.TextChannel): # This not is a DM or group DM # async with ctx.typing(): reason = "Topic edit by request of {author} ({id})" \ .format(author=ctx.author, id=ctx.author.id) try: await channel.edit(topic=topic, reason=reason) except (discord.Forbidden, discord.HTTPException): await ctx.send(content=error( "I don't have permission to edit this topic!")) return await ctx.send(content=info( "Channel {channel}'s topic has been updated.".format( channel=channel.mention))) else: # Private location: only reply with text await ctx.send( content=error("This channel type cannot have a topic."))
async def addsfx(self, ctx, link: str=None): """Adds a new sound. Either upload the file as a Discord attachment and make your comment "[p]addsfx", or use "[p]addsfx direct-URL-to-file". """ # await self.bot.type() server = ctx.message.guild serverid = str(server.id) if serverid not in os.listdir(self.sound_base): os.makedirs(os.path.join(self.sound_base, serverid)) if server.id not in self.settings: self.settings[server.id] = {} await self.save_json(self.settings_path, self.settings) attach = ctx.message.attachments if len(attach) > 1 or (attach and link): await ctx.send( cf.error("Please only add one sound at a time.")) return url = "" filename = "" if attach: a = attach[0] url = a.url filename = a.filename elif link: url = "".join(link) filename = os.path.basename( "_".join(url.split()).replace("%20", "_")) else: await ctx.send( cf.error("You must provide either a Discord attachment or a" " direct link to a sound.")) return filepath = os.path.join(self.sound_base, serverid, filename) if os.path.splitext(filename)[0] in self._list_sounds(serverid): await ctx.send( cf.error("A sound with that filename already exists." " Please change the filename and try again.")) return async with self.session.get(url) as new_sound: f = open(filepath, "wb") f.write(await new_sound.read()) f.close() self.settings[server.id][ os.path.splitext(filename)[0]] = {"volume": self.default_volume} await self.save_json(self.settings_path, self.settings) await ctx.send( cf.info("Sound {} added.".format(os.path.splitext(filename)[0])))
async def max_requestable(self, ctx, count : int): """Maximum number of roles that users can request.""" if count < 0: await ctx.send(error("Maximum must not be negative.")) await self.config.guild(ctx.guild).max_requestable.set(count) await ctx.send(info("Set maximum number of requestable roles per user to {}.".format(count)))
async def saucenao(self, ctx, image: ImageFinder = None): """Reverse search image via SauceNAO""" if image is None: try: image = await ImageFinder().search_for_images(ctx) except ValueError as e: await ctx.send(e) return image = image[0] try: search = await SauceNAO.from_image(ctx, image) except ValueError as e: await ctx.send(e) return if not search.results: await ctx.send(_("Nothing found")) return self.saucenao_limits["short"] = search.limits.short self.saucenao_limits["long"] = search.limits.long self.saucenao_limits["long_remaining"] = search.limits.remaining.long self.saucenao_limits["short_remaining"] = search.limits.remaining.short embeds = [] page = 0 for entry in search.results: page += 1 try: url = entry.urls[0] except IndexError: url = None # design is shit, ideas? e = discord.Embed( title=entry.source or entry.title or entry.service, description="\n".join([ _("Similarity: {}%").format(entry.similarity), "\n".join( [n for n in [entry.eng_name, entry.jp_name] if n]) or "", entry.part and _("Part/Episode: {}").format(entry.part) or "", entry.year and _("Year: {}").format(entry.year) or "", entry.est_time and _("Est. Time: {}").format(entry.est_time) or "", ]), url=url, color=await ctx.embed_colour(), timestamp=entry.created_at or discord.Embed.Empty, ) e.set_footer( text=_("Via SauceNAO • Page {}/{}").format( page, search.results_returned), icon_url= "https://www.google.com/s2/favicons?domain=saucenao.com", ) e.set_thumbnail(url=entry.thumbnail) embeds.append(e) if embeds: await menu(ctx, embeds, DEFAULT_CONTROLS) else: await ctx.send(chat.info(_("Nothing found")))
async def tracemoe(self, ctx, image: ImageFinder = None): """Reverse search image via WAIT If search performed not in NSFW channel, NSFW results will be not shown""" if image is None: try: image = await ImageFinder().search_for_images(ctx) except ValueError as e: await ctx.send(e) return image = image[0] try: search = await TraceMoe.from_image(ctx, image) except ValueError as e: await ctx.send(e) return embeds = [] page = 0 for doc in search.docs: page += 1 if getattr(ctx.channel, "nsfw", True) and doc.is_adult: continue # NOTE: Probably someone will come up with better embed design, but not me e = discord.Embed( title=doc.title, description="\n".join( s for s in [ _("Similarity: {:.2f}%").format(doc.similarity * 100), doc.title_native and "🇯🇵 " + _("Native title: {}").format(doc.title_native), doc.title_romaji and "🇯🇵 " + _("Romaji transcription: {}").format(doc.title_romaji), doc.title_chinese and "🇨🇳 " + _("Chinese title: {}").format(doc.title_chinese), doc.title_english and "🇺🇸 " + _("English title: {}").format(doc.title_english), _("Est. Time: {}").format(doc.time_str), _("Episode: {}").format(doc.episode), doc.synonyms and _("Also known as: {}").format(", ".join(doc.synonyms)), ] if s ), url=doc.mal_id and f"https://myanimelist.net/anime/{doc.mal_id}" or f"https://anilist.co/anime/{doc.anilist_id}", color=await ctx.embed_color(), ) e.set_thumbnail(url=doc.thumbnail) e.set_footer( text=_("Via WAIT (trace.moe) • Page {}/{}").format(page, len(search.docs)), icon_url="https://trace.moe/favicon128.png", ) embeds.append(e) if embeds: ctx.search_docs = search.docs await menu(ctx, embeds, TRACEMOE_MENU_CONTROLS) else: await ctx.send(chat.info(_("Nothing found")))
async def mess_bulk(self, ctx): """Toggle saving of bulk message deletion""" save_bulk = self.config.guild(ctx.guild).save_bulk await save_bulk.set(not await save_bulk()) state = _("enabled") if await self.config.guild( ctx.guild).save_bulk() else _("disabled") await ctx.send( chat.info(_("Bulk message removal saving {}").format(state)))
async def mess_edit(self, ctx): """Toggle logging of message editing""" editing = self.config.guild(ctx.guild).editing await editing.set(not await editing()) state = _("enabled") if await self.config.guild( ctx.guild).editing() else _("disabled") await ctx.send(chat.info( _("Message editing logging {}").format(state)))
async def mess_delete(self, ctx): """Toggle logging of message deletion""" deletion = self.config.guild(ctx.guild).deletion await deletion.set(not await deletion()) state = _("enabled") if await self.config.guild( ctx.guild).deletion() else _("disabled") await ctx.send( chat.info(_("Message deletion logging {}").format(state)))