Skip to content

PyDrocsid.embeds

send_long_embed async

send_long_embed(channel: Messageable | Message | InteractionResponse, embed: Embed, *, content: str | None = None, repeat_title: bool = False, repeat_thumbnail: bool = False, repeat_name: bool = False, repeat_image: bool = False, repeat_footer: bool = False, paginate: bool = False, pagination_user: User | Member | None = None, max_fields: int = 25, **kwargs: Any) -> list[Message]

Split and send a long embed in multiple messages.

Parameters:

  • channel (Messageable | Message | InteractionResponse) –

    the channel into which the messages should be sent

  • embed (Embed) –

    the embed to send

  • content (str | None) –

    the content of the first message

  • repeat_title (bool) –

    whether to repeat the embed title in every embed

  • repeat_thumbnail (bool) –

    whether to repeat the thumbnail image in every embed

  • repeat_name (bool) –

    whether to repeat field names in every embed

  • repeat_image (bool) –

    whether to repeat the image in every embed

  • repeat_footer (bool) –

    whether to repeat the footer in every embed

  • paginate (bool) –

    whether to use pagination instead of multiple messages

  • pagination_user (User | Member | None) –

    the user who should be able to control the pagination

  • max_fields (int) –

    the maximum number of fields an embed is allowed to have

Returns:

  • list[Message]

    list of all messages that have been sent

Source code in PyDrocsid/embeds.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
async def send_long_embed(
    channel: Messageable | Message | InteractionResponse,
    embed: Embed,
    *,
    content: str | None = None,
    repeat_title: bool = False,
    repeat_thumbnail: bool = False,
    repeat_name: bool = False,
    repeat_image: bool = False,
    repeat_footer: bool = False,
    paginate: bool = False,
    pagination_user: User | Member | None = None,
    max_fields: int = 25,
    **kwargs: Any,
) -> list[Message]:
    """
    Split and send a long embed in multiple messages.

    :param channel: the channel into which the messages should be sent
    :param embed: the embed to send
    :param content: the content of the first message
    :param repeat_title: whether to repeat the embed title in every embed
    :param repeat_thumbnail: whether to repeat the thumbnail image in every embed
    :param repeat_name: whether to repeat field names in every embed
    :param repeat_image: whether to repeat the image in every embed
    :param repeat_footer: whether to repeat the footer in every embed
    :param paginate: whether to use pagination instead of multiple messages
    :param pagination_user: the user who should be able to control the pagination
    :param max_fields: the maximum number of fields an embed is allowed to have
    :return: list of all messages that have been sent
    """

    if DISABLE_PAGINATION:
        paginate = False
        max_fields = EmbedLimits.FIELDS

    # enforce repeat_title, repeat_name and repeat_footer when using pagination
    if paginate:
        repeat_title = True
        repeat_name = True
        repeat_footer = True

    # always limit max_fields to 25
    max_fields = min(max_fields, EmbedLimits.FIELDS)

    # the maximum possible size of an embed
    max_total: int = EmbedLimits.TOTAL - 20 * paginate

    # pre checks
    if len(embed.title) > EmbedLimits.TITLE - 20 * paginate:
        raise ValueError("Embed title is too long.")
    if len(embed.url) > EmbedLimits.URL:
        raise ValueError("Embed url is too long.")
    if embed.thumbnail and len(embed.thumbnail.url) > EmbedLimits.THUMBNAIL_URL:
        raise ValueError("Thumbnail url is too long.")
    if embed.image and len(embed.image.url) > EmbedLimits.IMAGE_URL:
        raise ValueError("Image url is too long.")
    if embed.footer:
        if len(embed.footer.text) > EmbedLimits.FOOTER_TEXT:
            raise ValueError("Footer text is too long.")
        if len(embed.footer.icon_url) > EmbedLimits.FOOTER_ICON_URL:
            raise ValueError("Footer icon_url is too long.")
    if embed.author:
        if len(embed.author.name) > EmbedLimits.AUTHOR_NAME:
            raise ValueError("Author name is too long.")
        if len(embed.author.url) > EmbedLimits.AUTHOR_URL:
            raise ValueError("Author url is too long.")
        if len(embed.author.icon_url) > EmbedLimits.AUTHOR_ICON_URL:
            raise ValueError("Author icon_url is too long.")
    for i, field in enumerate(embed.fields):
        if len(field.name) > EmbedLimits.FIELD_NAME:
            raise ValueError(f"Name of field at position {i} is too long.")

    embeds: list[Embed] = []

    def add_embed(e: Embed) -> None:
        """Copy and add an embed to the list of embeds."""

        embeds.append(Embed.from_dict(deepcopy(e.to_dict())))

    def clear_embed(*, clear_completely: bool = False) -> None:
        if not repeat_title:
            cur.title = ""
            cur.remove_author()
        if not repeat_thumbnail:
            cur.set_thumbnail(url=EmptyEmbed)

        if clear_completely:
            cur.description = ""
            cur.clear_fields()

    # clear and backup embed fields, footer and image
    fields = embed.fields.copy()
    footer = embed.footer
    image = embed.image
    cur = embed.copy()
    cur.clear_fields()
    if not repeat_footer and footer:
        delattr(cur, "_footer")
    if not repeat_image:
        cur.set_image(url=EmptyEmbed)

    *parts, last = split_lines(cast(str, embed.description or ""), EmbedLimits.DESCRIPTION) or [""]
    for part in parts:
        cur.description = part
        add_embed(cur)
        clear_embed()

    cur.description = last

    # add embed fields
    for field in fields:
        parts = split_lines(field.value, EmbedLimits.FIELD_VALUE)
        inline = bool(field.inline) and len(parts) == 1

        if not field.name:
            field.name = EMPTY_MARKDOWN

        field_length: int = len(field.name) + sum(map(len, parts)) + len(EMPTY_MARKDOWN) * (len(parts) - 1)

        # check whether field fits in just one embed
        total_size_one_embed = field_length
        total_size_one_embed += len(cur.title)
        if cur.author:
            total_size_one_embed += len(cur.author.name)
        if cur.footer:
            total_size_one_embed += len(cur.footer.text)

        if len(parts) <= max_fields and total_size_one_embed <= max_total:

            if len(parts) + len(cur.fields) > max_fields or field_length + len(cur) > max_total:
                # field does not fit into current embed
                # -> create new embed
                add_embed(cur)
                clear_embed(clear_completely=True)

            # add field to current embed
            for i, part in enumerate(parts):
                cur.add_field(name=[field.name, EMPTY_MARKDOWN][i > 0], value=part, inline=inline)

        else:

            # add field parts individually
            for i, part in enumerate(parts):
                name: str = [field.name, EMPTY_MARKDOWN][i > 0]

                # check whether embed is full
                if len(cur.fields) >= max_fields or len(cur) + len(name) + len(part) > max_total:
                    # create new embed
                    add_embed(cur)
                    clear_embed(clear_completely=True)
                    if repeat_name:
                        name = field.name

                # add field part
                cur.add_field(name=name, value=part, inline=inline)

    # add footer to last embed (if previously removed)
    if not repeat_footer and footer:
        if len(cur) + len(footer.text) > max_total:
            add_embed(cur)
            clear_embed(clear_completely=True)

        cur.set_footer(text=footer.text, icon_url=footer.icon_url)

    # add image to last embed (if previously removed)
    if not repeat_image and image:
        cur.set_image(url=image.url)

    add_embed(cur)

    # don't use pagination if there is only one embed
    if not paginate or len(embeds) <= 1:
        messages = [
            await reply(channel, embed=e, content=content if not i else None, **kwargs) for i, e in enumerate(embeds)
        ]
    else:
        # create pagination
        if not pagination_user and isinstance(channel, (Message, Context)):
            pagination_user = channel.author

        messages = [await create_pagination(channel, pagination_user, embeds, **kwargs)]

    return [msg for msg in messages if msg]

split_lines

split_lines(text: str, max_size: int, *, first_max_size: int | None = None) -> list[str]

Split a string into a list of substrings such that each substring contains no more than max_size characters. To achieve this, this function first tries to split the string at line breaks. Substrings which are still too long are split at spaces and (only if necessary) between any two characters.

Parameters:

  • text (str) –

    the string

  • max_size (int) –

    maximum number of characters allowed in each substring

  • first_max_size (int | None) –

    optional override for max_size of first substring

Returns:

Source code in PyDrocsid/embeds.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def split_lines(text: str, max_size: int, *, first_max_size: int | None = None) -> list[str]:
    """
    Split a string into a list of substrings such that each substring contains no more than max_size characters.
    To achieve this, this function first tries to split the string at line breaks. Substrings which are still too
    long are split at spaces and (only if necessary) between any two characters.

    :param text: the string
    :param max_size: maximum number of characters allowed in each substring
    :param first_max_size: optional override for max_size of first substring
    :return: list of substrings
    """

    # strip any leading or trailing spaces and newlines
    text = text.strip(" \n")

    # max size of current substring
    ms: int = first_max_size or max_size

    # list of substrings
    out: list[str] = []

    # current position in string
    i = 0
    while i + ms < len(text):
        # try to find a line break within the next ms + 1 characters
        j = text.rfind("\n", i, i + ms + 1)
        if j == -1:  # no line break could be found
            # try to find a space within the next ms + 1 characters
            j = text.rfind(" ", i, i + ms + 1)

        if j == -1:  # no line break or space could be found
            # split string after exactly ms characters
            j = i + ms
            out.append(text[i:j])
            i = j
        else:
            # split string after line break or space
            out.append(text[i:j])
            i = j + 1  # line break or space should not be include in next substring

        ms = max_size

    # strip leading or trailing spaces and newlines from substring and remove empty strings
    return [y for x in out + [text[i:]] if (y := x.strip(" \n"))]