async def charinfo(self, ctx: commands.Context, *characters: Union[discord.PartialEmoji, str]): if not characters: await ctx.send_help() return characters = flatten( [x if isinstance(x, discord.PartialEmoji) else list(x) for x in characters] ) characters = deduplicate_iterables( [ x for x in characters # filter out space characters, as these usually aren't what we're being requested # to retrieve info on, and it's almost entirely useless to use an escape # sequence to get a mere space character if x != " " ] ) if len(characters) > 25: await ctx.send_help() return # the following is - for all intents and purposes - ripped directly from RDanny: # https://github.com/Rapptz/RoboDanny/blob/ee101d1/cogs/meta.py#L209-L223 def to_str(char: Union[commands.PartialEmojiConverter, str]): if isinstance(char, discord.PartialEmoji): return f"{char} \N{EM DASH} `{char.id}`" else: digit = ord(char) name = unicodedata.name(char, translate("name_not_found")) return f"{char} \N{EM DASH} `{name}` (`\\U{digit:>08}`)" await ctx.send("\n".join(map(to_str, characters)))
async def rift_link(self, ctx: commands.Context, one_way: Optional[bool] = None, *rifts: Messageable): """ Links this channel to the specified destination(s). Anything anyone says in this channel will be forwarded. All replies will be relayed back here. """ if not rifts: raise commands.UserInputError() unique_rifts: List[Messageable] = deduplicate_iterables(rifts) source = ctx.channel if ctx.guild else ctx.author no_notify = await self.bot.is_owner( ctx.author) and not await self.config.notify() for rift in unique_rifts: if (no_notify and getattr(rift, "guild", None) and not isinstance(rift, discord.abc.User)): mem = rift.guild.get_member(ctx.author.id) if mem and rift.permissions_for(mem).read_messages: notify = False else: notify = True else: notify = True self.rifts.add_vectors(source, rift, two_way=not one_way) if notify: asyncio.ensure_future(rift.send(_("").format())) await ctx.send( _("A link has been created to {}! Everything said in this channel will be relayed there.\n" "Responses will be relayed here.\n" "Type `exit` to quit.").format( humanize_list(list(map(str, unique_rifts)))))
async def on_message_without_command(self, message: discord.Message): if message.author.bot: return if message.type != discord.MessageType.default: return if not message.content and not message.attachments: return if not await self.bot.message_eligible_as_command(message): return channel = message.channel if message.guild else message.author destinations = deduplicate_iterables( self.rifts.get(Limited(message=message), ()), self.rifts.get(channel, ())) if not destinations: return if message.content.casefold() == "exit": if await can_close(message, self.bot): if num := await self.close_rifts(message.author, channel): return await message.channel.send( _("{num} rifts closed.").format(num=num)) else: if num := await self.close_rifts(message.author, Limited(message=message)): return await message.channel.send( _("{num} rifts closed.").format(num=num))
async def rift_web(self, ctx: commands.Context, *rifts: Messageable): """ Opens up all possible connections between this channel and the specified rifts. See the helptext of `[p]rift link` for more info. """ unique_rifts: List[Messageable] = deduplicate_iterables(rifts) source = ctx.channel if ctx.guild else ctx.author no_notify = await self.bot.is_owner( ctx.author) and not await self.config.notify() self.rifts.add_web(source, *unique_rifts) humanized = humanize_list(list(map(str, (source, *unique_rifts)))) for rift in unique_rifts: if (no_notify and getattr(rift, "guild", None) and not isinstance(rift, discord.abc.User)): assert isinstance(rift, discord.TextChannel) mem = rift.guild.get_member(ctx.author.id) if mem and rift.permissions_for(mem).read_messages: notify = False else: notify = True else: notify = True if notify: asyncio.ensure_future( rift.send( _("{} has opened a web to here, connecting you to {}." ).format(ctx.author, humanized))) await ctx.send( _("A web has been opened to {}! Everything you say will be relayed there.\n" "Responses will be relayed here.\n" "Type `exit` to quit.").format( humanize_list(list(map(str, unique_rifts)))))
async def on_typing(self, channel: UnionChannel, user: UnionUser, when: datetime): if user.bot: return destinations = deduplicate_iterables( self.rifts.get(Limited(author=user, channel=channel), ()), self.rifts.get(channel, ())) await asyncio.gather(*(channel.trigger_typing() for channel in destinations), return_exceptions=True)
async def user_defined_paths(self) -> List[Path]: """Get a list of user-defined cog paths. All paths will be absolute and unique, in order of priority. Returns ------- List[pathlib.Path] A list of user-defined paths. """ return list(map(Path, deduplicate_iterables(await self.conf.paths())))
async def send(self, ctx: commands.Context, *rifts: Messageable): """ Send a message to the specified destinations. Editing or deleting the message you send will still forward to the bot's reposts, as in normal rifts. """ if not rifts: raise commands.UserInputError() unique_rifts = deduplicate_iterables(rifts) await ctx.send("What would you like to say?") p = MessagePredicate.same_context(ctx=ctx) message = await ctx.bot.wait_for("message", check=p) await self._send(message, unique_rifts)
def names(self) -> Tuple[str, ...]: """Tuple containing the names of all usable locales the bot is configured to load from Locales in this tuple are matched from the return value of :attr:`LocaleProxy.bot` to the closest available on disk, and as such are guaranteed to exist on disk, but cannot be guaranteed to be actually loadable or contain any useful strings. The returned tuple is guaranteed to be free of duplicates. """ available = self.loader.available_locales() return tuple(x for x in deduplicate_iterables([ closest_locale(x, available, default=None) for x in (*self.bot, self.default) ]) if x is not None)
async def sl_reload(self, ctx: commands.Context, skip_confirmation: bool = False): self.dump(".".join(self.__module__.split(".")[:-1])) self.dump("swift_i18n") to_reload = [x.lower() for x in dependents if x in ctx.bot.cogs] to_reload.extend(map(lambda x: x.lower(), require_update)) to_reload = deduplicate_iterables(to_reload) names = " ".join(inline(x) for x in to_reload) if skip_confirmation or await confirm( ctx, content=translate("reload_confirm", cogs=names)): await ctx.invoke(ctx.bot.get_cog("Core").reload, *to_reload) else: await ctx.send(translate("reload_manual", cogs=names))
async def paths(self) -> List[Path]: """Get all currently valid path directories, in order of priority Returns ------- List[pathlib.Path] A list of paths where cog packages can be found. The install path is highest priority, followed by the user-defined paths, and the core path has the lowest priority. """ return deduplicate_iterables( [await self.install_path()], await self.user_defined_paths(), [self.CORE_PATH] )
async def paths(self) -> Tuple[Path, ...]: """Get all currently valid path directories. Returns ------- `tuple` of `pathlib.Path` All valid cog paths. """ conf_paths = [Path(p) for p in await self.conf.paths()] other_paths = self._paths all_paths = deduplicate_iterables(conf_paths, other_paths, [self.CORE_PATH]) if self.install_path not in all_paths: all_paths.insert(0, await self.install_path()) return tuple(p.resolve() for p in all_paths if p.is_dir())
async def rift_open( self, ctx: commands.Context, one_way: Optional[bool] = None, *rifts: Messageable ): """ Opens a rift to the specified destination(s). Only your messages will be forwarded to the specified destinations, and all replies will be sent back to you. """ if not rifts: raise commands.UserInputError() unique_rifts: List[Messageable] = deduplicate_iterables(rifts) source = Limited(message=ctx.message) if ctx.guild else ctx.author no_notify = await self.bot.is_owner(ctx.author) and not await self.config.notify() for rift in unique_rifts: if ( no_notify and getattr(rift, "guild", None) and not isinstance(rift, discord.abc.User) ): mem = rift.guild.get_member(ctx.author.id) if mem and rift.permissions_for(mem).read_messages: notify = False else: notify = True else: notify = True self.rifts.add_vectors(source, rift, two_way=not one_way) if notify: asyncio.ensure_future( rift.send( _("{} has opened a rift to here from {}.").format(ctx.author, ctx.channel) ) ) await ctx.send( _( "A rift has been opened to {}! Everything you say will be relayed there.\n" "Responses will be relayed here.\n" "Type `exit` to quit." ).format(humanize_list(list(map(str, unique_rifts)))) )
def test_deduplicate_iterables(): expected = [1, 2, 3, 4, 5] inputs = [[1, 2, 1], [3, 1, 2, 4], [5, 1, 2]] assert deduplicate_iterables(*inputs) == expected
async def ao3(self, ctx, ficlink, *, notes=""): """Returns details of a fic from a link If the fic you inputted is wrong, just click the ❎ emoji to delete the message (Needs Manage Messages permissions).""" # SET NOTES if notes == "": notes = "None." else: nlimit = await self.config.guild(ctx.guild).noteslimit() notes = notes[:nlimit] # GET URL if "chapter" in ficlink: newlink = ficlink.split("chapters")[0] ficlink = str(newlink) if "collections" in ficlink: newlink = ficlink.split("/works/")[1] ficlink = str(f"https://archiveofourown.org/works/{newlink}") if "?view_full_work=true" in ficlink: newlink = ficlink.split("?")[0] ficlink = str(newlink) firstchap = f"{ficlink}/navigate" async with self.session.get(firstchap) as ao3navigation: navigate = BeautifulSoup(await ao3navigation.text(), 'html.parser', parse_only=SoupStrainer("ol")) try: firstchap = navigate.find("li").a['href'] url = f"https://archiveofourown.org{firstchap}?view_adult=true" except AttributeError: return await ctx.send( "Error loading work info. Please ensure that the work is not locked." ) # START SCRAPING async with self.session.get(url) as ao3session: result = BeautifulSoup(await ao3session.text(), 'html.parser') # GET AUTHORS try: a = result.find_all("a", {'rel': 'author'}) author_list = [] for author in a: author_list.append(author.string.strip()) try: authors = humanize_list(deduplicate_iterables(author_list)) except Exception: authors = "Anonymous" except Exception: return await ctx.send("Error loading author list.") # GET TITLE try: preface = result.find("div", {'class': 'preface group'}).h2.string title = str(preface.strip()) except Exception: title = "No title found." # GET FANDOM try: fan = result.find("dd", {'class': 'fandom tags'}) fan_list = [] fandomlimit = await self.config.guild(ctx.guild).fandomlimit() for fandom in fan.find_all("li", limit=fandomlimit): fan_list.append(fandom.a.string) fandom = humanize_list(fan_list) except Exception: fandom = "No fandom found." # GET PAIRING try: reltags = result.find("dd", {'class': 'relationship tags'}) pair_list = [] pairlimit = await self.config.guild(ctx.guild).pairlimit() for rel in reltags.find_all("li", limit=pairlimit): pair_list.append(rel.a.string) pairing = humanize_list(pair_list) except Exception: pairing = "No Pairing." # GET CHAPTERS chapters = result.find("dd", {'class': 'chapters'}) totalchapters = str(BeautifulSoup.getText(chapters)) # GET STATUS chap_list = totalchapters.split("/") if "?" in chap_list[1]: status = "Work in Progress" elif chap_list[0] != chap_list[1]: status = "Work in Progress" else: status = "Complete" # GET RATING try: rate = result.find("dd", {'class': 'rating tags'}) rating = rate.a.string except Exception: rating = "Not Rated" # GET SUMMARY try: div = result.find("div", {'class': 'preface group'}) userstuff = div.find("blockquote", {'class': 'userstuff'}) stuff = str(BeautifulSoup.getText(userstuff)) summarytest = f"{stuff}".replace('. ', '**').replace('.', '. ') summ = f"{summarytest}".replace('**', '. \n\n') slimit = await self.config.guild(ctx.guild).sumlimit() summary = summ[:slimit] except Exception: summary = "No work summary found." # GET TAGS try: use_censor = await self.config.guild(ctx.guild).censor() freeform = result.find("dd", {'class': 'freeform tags'}) tag_list = [] taglimit = await self.config.guild(ctx.guild).taglimit() for tag in freeform.find_all("li", limit=taglimit): tag_list.append(tag.a.string) if "Explicit" in rating and use_censor: tags = f"||{(humanize_list(tag_list))}||" else: tags = humanize_list(tag_list) except Exception: tags = "No tags found." # GET DATE PUBLISHED AND UPDATED published = result.find("dd", {'class': 'published'}).string.strip() try: updated = result.find("dd", {'class': 'status'}).string.strip() except Exception: updated = published # GET LANGUAGE language = result.find("dd", {'class': 'language'}).string.strip() # GET WORDS words = int( result.find("dd", { 'class': 'words' }).string.replace(",", "")) # GET KUDOS try: kudos = int( result.find("dd", { 'class': 'kudos' }).string.replace(",", "")) except AttributeError: kudos = 0 # GET HITS try: hits = int( result.find("dd", { 'class': 'hits' }).string.replace(",", "")) except AttributeError: hits = 0 # GET WARNINGS warntags = result.find("dd", {'class': 'warning tags'}) warn_list = [] try: for warning in warntags.find_all("li"): warn_list.append(warning.a.string) warnings = humanize_list(warn_list) except Exception: warnings = "No warnings found." # CHECK INFO FORMAT use_embed = await self.config.guild(ctx.guild).embed() data = await self.config.guild(ctx.guild).formatting() if use_embed: data = discord.Embed(description=summary, title=title, url=ficlink, colour=3553599) data.add_field(name="Author:", value=authors, inline=False) data.add_field(name="Fandom:", value=fandom, inline=False) data.add_field(name="Rating:", value=rating, inline=False) data.add_field(name="Pairings:", value=pairing, inline=False) data.add_field(name="Tags:", value=tags, inline=False) data.add_field(name=f"Rec Notes by {ctx.author}: ", value=notes, inline=False) data.set_footer( text= f"Language: {language} | Words: {words} | Date Updated: {updated} | Status: {status} " ) ao3msg = await ctx.send(embed=data) else: params = { "title": title, "authors": authors, "rating": rating, "warnings": warnings, "language": language, "fandom": fandom, "pairing": pairing, "tags": tags, "summary": summary, "totalchapters": totalchapters, "status": status, "words": words, "kudos": kudos, "hits": hits, "reccer": ctx.author.mention, "notes": notes, "url": f"<{ficlink}>", "published": published, "updated": updated } ao3msg = await ctx.send(data.format(**params)) start_adding_reactions(ao3msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(ao3msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=30) await self._clear_react(ao3msg) if pred.result is False: await ao3msg.delete() return except asyncio.TimeoutError: await self._clear_react(ao3msg) autodel = await self.config.guild(ctx.guild).autodelete() try: if autodel is True: await ctx.message.delete() return except Exception: return
async def trends( self, ctx: commands.Context, timeframe: Optional[TimeframeConverter] = "now 7-d", geo: Optional[GeoConverter] = "", *query: str, ): """ Find what the world is searching, right from Discord. **Get started with `[p]trends discord` for a basic example!** **Optional** `timeframe`: You can specify either the long (eg `4hours`) or short (`eg 4h`) version of the timeframes. All other values not listed below are invalid. `hour`/`1h` `4hours`/`4h` `day`/`1d` `week`/`7d` `month`/`1m` `3months`/`3m` `12months`/`12m` `5years`/`5y` `all` `geo`: Defaults to `world` You can specify a two-letter geographic code, such as `US`, `GB` or `FR`. Sometimes, you can also add a sub-region. See https://go.vexcodes.com/trends_geo for a list. **Required** `trends`: Whatever you want! You can add multiple trends, and separate them with a space. If your trend has spaces in it, you can use `+` instead of a space or enclose it in quotes, for example `Card games` to `Card+games` or `"Card games"`. **Examples:** The help message is so long that examples wouldn't fit! Run `[p]trendsexamples` to see some. """ if timeframe is None or geo is None: # should never happen return query = deduplicate_iterables(query) if len(query ) == 0 and geo != "" and timeframe != "now 7-d": # not defaults await ctx.send( "You must specify at least one query. For example, `[p]trends discord`" ) return elif len(query) == 0: await ctx.send_help() return if len(query) > 5: await ctx.send("Sorry, the maximum about of queries is 5") return async with ctx.typing(): try: request = await self.get_trends_request( list(query), timeframe, geo) except ResponseError as e: if e.response.status_code == 400: await ctx.send( "Your request failed. It looks like something's invalid." ) elif e.response.status_code in (403, 429): await ctx.send( "Your request failed. It looks like we've hit a rate limit. " "Try again in a minute.") else: await ctx.send( "Your request failed for an unexpected reason.") return try: file = await self.plot_graph(request, timeframe, geo) except NoData: await ctx.send( "Sorry, there's no significant data for that. Check your spelling or choose " "another query.") return full_location = [k for k, v in GEOS.items() if v == geo][0] full_timeframe = TIMEFRAMES.get(timeframe, "") url = self.get_trends_url(timeframe, geo, query) embed = discord.Embed( title=f"Interest over time, {full_location}, {full_timeframe}", colour=await ctx.embed_colour(), ) embed.set_footer( text= f"Times are in UTC\nSee {ctx.clean_prefix}help trends for advanced usage\n" + "Data sourced from Google Trends.") embed.set_image(url="attachment://plot.png") button = url_buttons.URLButton("View on Google Trends", url) await url_buttons.send_message(self.bot, ctx.channel.id, embed=embed, file=file, url_button=button)
async def aliases(self, ctx: commands.Context, *, strcommand: str): """ Get all the alias information you could ever want about a command. This will show the main command, built-in aliases, global aliases and server aliases. """ command = self.bot.get_command(strcommand) alias_cog = self.bot.get_cog("Alias") if alias_cog is None: if command is None: return await ctx.send("Hmm, I can't find that command.") full_com = command.qualified_name builtin_aliases = command.aliases com_parent = command.parent or "" com_builtin_aliases = [ inline(f"{com_parent} {builtin_aliases[i]}") for i in range(len(builtin_aliases)) ] msg = "I was unable to get information from the alias cog. It's probably not loaded.\n" msg += f"Main command: `{full_com}`\nBuilt in aliases: " msg += humanize_list(com_builtin_aliases) return await ctx.send(msg) alias_conf: Config = alias_cog.config # type:ignore all_global_aliases: List[dict] = await alias_conf.entries() if ctx.guild: all_guild_aliases: List[dict] = await alias_conf.guild(ctx.guild ).entries() else: all_guild_aliases = [] # check if command is actually from alias cog if command is None: for alias in all_guild_aliases: if alias["name"] == strcommand: command = self.bot.get_command(alias["command"]) for alias in all_global_aliases: if alias["name"] == strcommand: command = self.bot.get_command(alias["command"]) if command is None: return await ctx.send("That's not a command or alias.") builtin_aliases = command.aliases com_parent = command.parent or "" guild_aliases = [ alias["name"] for alias in all_guild_aliases if strcommand in [alias["command"], alias["name"]] ] global_aliases = [ alias["name"] for alias in all_global_aliases if strcommand in [alias["command"], alias["name"]] ] # and probs picked up duplicates on second run so: guild_aliases = deduplicate_iterables(guild_aliases) guild_aliases = [ i for i in guild_aliases if not self.bot.get_command(i) ] global_aliases = deduplicate_iterables(global_aliases) global_aliases = [ i for i in global_aliases if not self.bot.get_command(i) ] # make everything inline + make built in aliases hum_builtin_aliases = inline_hum_list( [f"{com_parent} {i}" for i in builtin_aliases]) hum_global_aliases = inline_hum_list(global_aliases) hum_guild_aliases = inline_hum_list(guild_aliases) aliases = "" none = [] if hum_builtin_aliases: aliases += f"Built-in aliases: {hum_builtin_aliases}\n" else: none.append("built-in") if hum_global_aliases: aliases += f"Global aliases: {hum_global_aliases}\n" else: none.append("global") if hum_guild_aliases: aliases += f"Server aliases: {hum_guild_aliases}\n" else: if ctx.guild: none.append("guild") else: aliases += "You're in DMs, so there aren't any server aliases." str_none = humanize_list(none, style="or") msg = f"Main command: `{strcommand}`\n{aliases}" if str_none: msg += f"This command has no {str_none} aliases." pages = pagify(msg, delims=["\n", ", "]) for page in pages: await ctx.send(page)
def get_update_ids(self) -> list[str]: """Get the group IDs for this feed, in order.""" return deduplicate_iterables( [field.update_id for field in self.fields])