async def handle_alias_required_licenses(ctx, err): embed = EmbedWithAuthor(ctx) if not err.has_connected_ddb: # was the user blocked from nSRD by a feature flag? ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id) if ddb_user is None: blocked_by_ff = False else: blocked_by_ff = not (await ctx.bot.ldclient.variation( "entitlements-enabled", ddb_user.to_ld_dict(), False)) if blocked_by_ff: embed.title = "D&D Beyond is currently unavailable" embed.description = f"I was unable to communicate with D&D Beyond to confirm access to:\n" \ f"{', '.join(e.name for e in err.entities)}" else: embed.title = f"Connect your D&D Beyond account to use this customization!" embed.url = "https://www.dndbeyond.com/account" embed.description = \ "This customization requires access to one or more entities that are not in the SRD.\n" \ "Linking your account means that you'll be able to use everything you own on " \ "D&D Beyond in Avrae for free - you can link your accounts " \ "[here](https://www.dndbeyond.com/account)." embed.set_footer( text= "Already linked your account? It may take up to a minute for Avrae to recognize the " "link.") else: missing_source_ids = {e.source for e in err.entities} if len(err.entities) == 1: # 1 entity, display entity piecemeal embed.title = f"Purchase {err.entities[0].name} on D&D Beyond to use this customization!" marketplace_url = err.entities[0].marketplace_url elif len(missing_source_ids ) == 1: # 1 source, recommend purchasing source missing_source = next(iter(missing_source_ids)) embed.title = f"Purchase {long_source_name(missing_source)} on D&D Beyond to use this customization!" marketplace_url = f"https://www.dndbeyond.com/marketplace?utm_source=avrae&utm_medium=marketplacelink" else: # more than 1 source embed.title = f"Purchase {len(missing_source_ids)} sources on D&D Beyond to use this customization!" marketplace_url = "https://www.dndbeyond.com/marketplace?utm_source=avrae&utm_medium=marketplacelink" missing = natural_join( [f"[{e.name}]({e.marketplace_url})" for e in err.entities], "and") if len(missing) > 1400: missing = f"{len(err.entities)} items" missing_sources = natural_join( [long_source_name(e) for e in missing_source_ids], "and") embed.description = \ f"To use this customization and gain access to more integrations in Avrae, unlock **{missing}** by " \ f"purchasing {missing_sources} on D&D Beyond.\n\n" \ f"[Go to Marketplace]({marketplace_url})" embed.url = marketplace_url embed.set_footer( text= "Already purchased? It may take up to a minute for Avrae to recognize the " "purchase.") await ctx.send(embed=embed)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" choices = await get_item_choices(ctx, filter_by_license=False) item = await self._lookup_search3(ctx, {'magic-item': choices}, name, query_type='item') embed = EmbedWithAuthor(ctx) embed.title = item.name embed.url = item.url embed.description = item.meta if item.attunement: if item.attunement is True: # can be truthy, but not true embed.add_field(name="Attunement", value=f"Requires Attunement") else: embed.add_field(name="Attunement", value=f"Requires Attunement {item.attunement}", inline=False) text = trim_str(item.desc, 5500) add_fields_from_long_text(embed, "Description", text) if item.image: embed.set_thumbnail(url=item.image) handle_source_footer(embed, item, "Item") await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
async def unsubscribe(self, ctx, name): if self.before_edit_check: await self.before_edit_check(ctx) # get the collection by URL or name coll_match = re.match(WORKSHOP_ADDRESS_RE, name) if coll_match is None: # load all subscribed collections to search subscribed_collections = [] async for subscription_doc in self.workshop_sub_meth(ctx): try: coll = await workshop.WorkshopCollection.from_id( ctx, subscription_doc['object_id']) subscribed_collections.append(coll) except workshop.CollectionNotFound: continue the_collection = await search_and_select(ctx, subscribed_collections, name, key=lambda c: c.name) else: collection_id = coll_match.group(1) the_collection = await workshop.WorkshopCollection.from_id( ctx, collection_id) if self.is_server: await the_collection.unset_server_active(ctx) else: await the_collection.unsubscribe(ctx) embed = EmbedWithAuthor(ctx) embed.title = f"Unsubscribed from {the_collection.name}" embed.url = the_collection.url embed.description = the_collection.description await ctx.send(embed=embed)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" choices = await get_spell_choices(ctx, filter_by_license=False) spell = await self._lookup_search3(ctx, {'spell': choices}, name) embed = EmbedWithAuthor(ctx) embed.url = spell.url color = embed.colour embed.title = spell.name school_level = f"{spell.get_level()} {spell.get_school().lower()}" if spell.level > 0 \ else f"{spell.get_school().lower()} cantrip" embed.description = f"*{school_level}. " \ f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*" if spell.ritual: time = f"{spell.time} (ritual)" else: time = spell.time meta = f"**Casting Time**: {time}\n" \ f"**Range**: {spell.range}\n" \ f"**Components**: {spell.components}\n" \ f"**Duration**: {spell.duration}" embed.add_field(name="Meta", value=meta) text = spell.description higher_levels = spell.higherlevels if len(text) > 1020: pieces = [text[:1020]] + [ text[i:i + 2040] for i in range(1020, len(text), 2040) ] else: pieces = [text] embed.add_field(name="Description", value=pieces[0], inline=False) embed_queue = [embed] if len(pieces) > 1: for piece in pieces[1:]: temp_embed = discord.Embed() temp_embed.colour = color temp_embed.description = piece embed_queue.append(temp_embed) if higher_levels: add_fields_from_long_text(embed_queue[-1], "At Higher Levels", higher_levels) embed_queue[-1].set_footer(text=f"Spell | {spell.source_str()}") if spell.homebrew: add_homebrew_footer(embed_queue[-1]) if spell.image: embed_queue[0].set_thumbnail(url=spell.image) await Stats.increase_stat(ctx, "spells_looked_up_life") destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
async def subscribe(self, ctx, url): coll_match = re.match(WORKSHOP_ADDRESS_RE, url) if coll_match is None: return await ctx.send("This is not an Alias Workshop link.") if self.before_edit_check: await self.before_edit_check(ctx) collection_id = coll_match.group(1) the_collection = await workshop.WorkshopCollection.from_id( ctx, collection_id) # private and duplicate logic handled here, also loads aliases/snippets if self.is_server: await the_collection.set_server_active(ctx) else: await the_collection.subscribe(ctx) embed = EmbedWithAuthor(ctx) embed.title = f"Subscribed to {the_collection.name}" embed.url = the_collection.url embed.description = the_collection.description if the_collection.aliases: embed.add_field( name="Server Aliases" if self.is_server else "Aliases", value=", ".join(sorted(a.name for a in the_collection.aliases))) if the_collection.snippets: embed.add_field( name="Server Snippets" if self.is_server else "Snippets", value=", ".join(sorted(a.name for a in the_collection.snippets))) await ctx.send(embed=embed)
async def _class(self, ctx, name: str, level: int = None): """Looks up a class, or all features of a certain level.""" if level is not None and not 0 < level < 21: return await ctx.send("Invalid level.") result: gamedata.Class = await self._lookup_search3( ctx, {'class': compendium.classes}, name) embed = EmbedWithAuthor(ctx) embed.url = result.url if level is None: embed.title = result.name embed.add_field(name="Hit Points", value=result.hit_points) levels = [] for level in range(1, 21): level = result.levels[level - 1] levels.append(', '.join([feature.name for feature in level])) embed.add_field(name="Starting Proficiencies", value=result.proficiencies, inline=False) embed.add_field(name="Starting Equipment", value=result.equipment, inline=False) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str handle_source_footer( embed, result, f"Use {ctx.prefix}classfeat to look up a feature.", add_source_str=False) else: embed.title = f"{result.name}, Level {level}" level_features = result.levels[level - 1] for resource, value in zip(result.table.headers, result.table.levels[level - 1]): if value != '0': embed.add_field(name=resource, value=value) for f in level_features: embed.add_field(name=f.name, value=trim_str(f.text, 1024), inline=False) handle_source_footer( embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.", add_source_str=False) await (await self._get_destination(ctx)).send(embed=embed)
async def feat(self, ctx, *, name: str): """Looks up a feat.""" result: gamedata.Feat = await self._lookup_search3(ctx, {'feat': compendium.feats}, name) embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url if result.prerequisite: embed.add_field(name="Prerequisite", value=result.prerequisite, inline=False) add_fields_from_long_text(embed, "Description", result.desc) handle_source_footer(embed, result, "Feat") await (await self._get_destination(ctx)).send(embed=embed)
async def classfeat(self, ctx, *, name: str): """Looks up a class feature.""" result: SourcedTrait = await self._lookup_search3(ctx, {'class': compendium.cfeats}, name, query_type='classfeat') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url set_maybe_long_desc(embed, result.text) handle_source_footer(embed, result, "Class Feature") await (await self._get_destination(ctx)).send(embed=embed)
async def racefeat(self, ctx, *, name: str): """Looks up a racial feature.""" result: SourcedTrait = await self._lookup_search3(ctx, {'race': compendium.rfeats, 'subrace': compendium.subrfeats}, name, 'racefeat') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url set_maybe_long_desc(embed, result.text) handle_source_footer(embed, result, "Race Feature") await (await self._get_destination(ctx)).send(embed=embed)
async def background(self, ctx, *, name: str): """Looks up a background.""" result: gamedata.Background = await self._lookup_search3(ctx, {'background': compendium.backgrounds}, name) embed = EmbedWithAuthor(ctx) embed.url = result.url embed.title = result.name handle_source_footer(embed, result, "Background") for trait in result.traits: text = trim_str(trait.text, 1024) embed.add_field(name=trait.name, value=text, inline=False) await (await self._get_destination(ctx)).send(embed=embed)
async def race(self, ctx, *, name: str): """Looks up a race.""" result: gamedata.Race = await self._lookup_search3(ctx, {'race': compendium.races, 'subrace': compendium.subraces}, name, 'race') embed = EmbedWithAuthor(ctx) embed.title = result.name embed.url = result.url embed.add_field(name="Speed", value=result.speed) embed.add_field(name="Size", value=result.size) for t in result.traits: add_fields_from_long_text(embed, t.name, t.text) handle_source_footer(embed, result, "Race") await (await self._get_destination(ctx)).send(embed=embed)
async def subclass(self, ctx, *, name: str): """Looks up a subclass.""" result: gamedata.Subclass = await self._lookup_search3(ctx, {'class': compendium.subclasses}, name, query_type='subclass') embed = EmbedWithAuthor(ctx) embed.url = result.url embed.title = result.name embed.description = f"*Source: {result.source_str()}*" for level in result.levels: for feature in level: text = trim_str(feature.text, 1024) embed.add_field(name=feature.name, value=text, inline=False) handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.", add_source_str=False) await (await self._get_destination(ctx)).send(embed=embed)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" choices = await get_spell_choices(ctx, filter_by_license=False) spell = await self._lookup_search3(ctx, {'spell': choices}, name) embed = EmbedWithAuthor(ctx) embed.url = spell.url color = embed.colour embed.title = spell.name school_level = f"{spell.get_level()} {spell.get_school().lower()}" if spell.level > 0 \ else f"{spell.get_school().lower()} cantrip" embed.description = f"*{school_level}. " \ f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*" if spell.ritual: time = f"{spell.time} (ritual)" else: time = spell.time meta = f"**Casting Time**: {time}\n" \ f"**Range**: {spell.range}\n" \ f"**Components**: {spell.components}\n" \ f"**Duration**: {spell.duration}" embed.add_field(name="Meta", value=meta) higher_levels = spell.higherlevels pieces = chunk_text(spell.description) embed.add_field(name="Description", value=pieces[0], inline=False) embed_queue = [embed] if len(pieces) > 1: for i, piece in enumerate(pieces[1::2]): temp_embed = discord.Embed() temp_embed.colour = color if (next_idx := (i + 1) * 2) < len( pieces ): # this is chunked into 1024 pieces, and descs can handle 2 temp_embed.description = piece + pieces[next_idx] else: temp_embed.description = piece embed_queue.append(temp_embed)
async def handle_required_license(ctx, err): """ Logs a unlicensed search and displays a prompt. :type ctx: discord.ext.commands.Context :type err: cogs5e.models.errors.RequiresLicense """ result = err.entity await ctx.bot.mdb.analytics_nsrd_lookup.update_one( { "type": result.entity_type, "name": result.name }, {"$inc": { "num_lookups": 1 }}, upsert=True) embed = EmbedWithAuthor(ctx) if not err.has_connected_ddb: # was the user blocked from nSRD by a feature flag? ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id) if ddb_user is None: blocked_by_ff = False else: blocked_by_ff = not (await ctx.bot.ldclient.variation( "entitlements-enabled", ddb_user.to_ld_dict(), False)) if blocked_by_ff: # get the message from feature flag # replacements: # $entity_type$, $entity_name$, $source$, $long_source$ unavailable_title = await ctx.bot.ldclient.variation( "entitlements-disabled-header", ddb_user.to_ld_dict(), f"{result.name} is not available") unavailable_desc = await ctx.bot.ldclient.variation( "entitlements-disabled-message", ddb_user.to_ld_dict(), f"{result.name} is currently unavailable") embed.title = unavailable_title \ .replace('$entity_type$', result.entity_type) \ .replace('$entity_name$', result.name) \ .replace('$source$', result.source) \ .replace('$long_source$', long_source_name(result.source)) embed.description = unavailable_desc \ .replace('$entity_type$', result.entity_type) \ .replace('$entity_name$', result.name) \ .replace('$source$', result.source) \ .replace('$long_source$', long_source_name(result.source)) else: embed.title = f"Connect your D&D Beyond account to view {result.name}!" embed.url = "https://www.dndbeyond.com/account" embed.description = \ "It looks like you don't have your Discord account connected to your D&D Beyond account!\n" \ "Linking your account means that you'll be able to use everything you own on " \ "D&D Beyond in Avrae for free - you can link your accounts " \ "[here](https://www.dndbeyond.com/account)." embed.set_footer( text= "Already linked your account? It may take up to a minute for Avrae to recognize the " "link.") else: embed.title = f"Purchase {result.name} on D&D Beyond to view it here!" embed.description = \ f"To see and search this {result.entity_type}'s full details, unlock **{result.name}** by " \ f"purchasing {long_source_name(result.source)} on D&D Beyond.\n\n" \ f"[Go to Marketplace]({result.marketplace_url})" embed.url = result.marketplace_url embed.set_footer( text= "Already purchased? It may take up to a minute for Avrae to recognize the " "purchase.") await ctx.send(embed=embed)
async def serve(self, ctx, name): # get the personal alias/snippet if self.is_alias: personal_obj = await helpers.get_personal_alias_named(ctx, name) check_coro = _servalias_before_edit else: personal_obj = await helpers.get_personal_snippet_named(ctx, name) check_coro = _servsnippet_before_edit if personal_obj is None: return await ctx.send( f"You do not have {a_or_an(self.obj_name)} named `{name}`.") await check_coro(ctx, name) # If the alias is a workshop alias we need to get the workshopCollection and set it as active. if not isinstance(personal_obj, self.personal_cls): await personal_obj.load_collection(ctx) collection = personal_obj.collection response = await confirm( ctx, f"This action will subscribe the server to the `{collection.name}` workshop collection, found at " f"<{collection.url}>. This will add {collection.alias_count} aliases and " f"{collection.snippet_count} snippets to the server. Do you want to continue? (Reply with yes/no)" ) if not response: return await ctx.send("Ok, aborting.") await collection.set_server_active( ctx) # this loads the aliases/snippets embed = EmbedWithAuthor(ctx) embed.title = f"Subscribed to {collection.name}" embed.url = collection.url embed.description = collection.description if collection.aliases: embed.add_field(name="Server Aliases", value=", ".join( sorted(a.name for a in collection.aliases))) if collection.snippets: embed.add_field(name="Server Snippets", value=", ".join( sorted(a.name for a in collection.snippets))) return await ctx.send(embed=embed) # else it's a personal alias/snippet if self.is_alias: existing_server_obj = await personal.Servalias.get_named( personal_obj.name, ctx) server_obj = personal.Servalias.new(personal_obj.name, personal_obj.code, ctx.guild.id) else: existing_server_obj = await personal.Servsnippet.get_named( personal_obj.name, ctx) server_obj = personal.Servsnippet.new(personal_obj.name, personal_obj.code, ctx.guild.id) # check if it overwrites anything if existing_server_obj is not None and not await confirm( ctx, f"There is already an existing server {self.obj_name} named `{name}`. Do you want to overwrite it? " f"(Reply with yes/no)"): return await ctx.send("Ok, aborting.") await server_obj.commit(ctx.bot.mdb) out = f'Server {self.obj_name} `{server_obj.name}` added.' \ f'```py\n{ctx.prefix}{self.obj_copy_command} {server_obj.name} {server_obj.code}\n```' if len(out) > 2000: out = f'Server {self.obj_name} `{server_obj.name}` added.' \ f'Command output too long to display.' await ctx.send(out)