Beispiel #1
0
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)
Beispiel #2
0
    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)
Beispiel #3
0
    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)
Beispiel #4
0
    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)
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
    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)
Beispiel #9
0
    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)
Beispiel #10
0
    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)
Beispiel #11
0
    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)
Beispiel #12
0
    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)
Beispiel #13
0
    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)
Beispiel #14
0
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)
Beispiel #15
0
    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)