1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
//! Discord's own length limits, named once, and the truncation they share.
//!
//! # Why this file exists for one small function
//!
//! Nine Discord limits have bitten this port. They are listed rather than
//! counted in a sentence, because a running tally has to be renumbered
//! every time one is added and the tenth is then one more line instead of
//! a re-count of this paragraph:
//!
//! - `custom_id` caps at 100 characters, so a button keys on a numeric
//! sheep id rather than a name. The cap stays local to
//! [`crate::bot::embed`]; see below.
//! - [`MESSAGE_CHARACTER_BUDGET`], the 6,000 character sum across every
//! embed on ONE message. [`crate::stream::pack`] exists for it, and
//! treating it as an assumption rather than an enforced budget cost a
//! whole fix round. `/shep list` is the second caller to need it.
//! - [`EMBED_TITLE_LIMIT`], caught in [`crate::bot::embed`] before this
//! file existed.
//! - [`FIELD_VALUE_LIMIT`]: [`crate::bot::embed::process_embed`] put an
//! operator-chosen fold or smit into a field with no cap at all, the
//! same shape of gap as an uncapped sheep name.
//! - [`EMBED_DESCRIPTION_LIMIT`], for the same reason one field down.
//! - [`MESSAGE_CONTENT_LIMIT`]: an error's `Display` went straight into a
//! followup's content with no bound of its own.
//! - [`EMBED_MAX_COUNT`], ten embeds on one message. A listing of many
//! small sheep reaches it long before it reaches the character sum, so
//! a packer watching only characters would take a 400 for the count.
//! - Discord's 25 suggestions per autocomplete response, kept local to
//! `/shep` since nothing else in this crate offers suggestions.
//! - [`AUTOCOMPLETE_CHOICE_NAME_LIMIT`], the one case in this file where
//! cutting a string is the wrong answer; see its own doc comment.
//! - The four slash command registration caps, [`COMMAND_NAME_LIMIT`],
//! [`COMMAND_DESCRIPTION_LIMIT`], [`OPTION_NAME_LIMIT`] and
//! [`OPTION_DESCRIPTION_LIMIT`], which apply at every nesting depth.
//! These have the worst failure mode of the ten and are the only ones
//! [`fit`] must NOT be used on; both halves of that are argued below.
//!
//! Ten encounters with one class of bug is a missing abstraction, not ten
//! unrelated ones, so the caps live here once and [`fit`] is the one
//! place a string gets cut down to one of them.
//!
//! # Why the registration caps are the dangerous ones
//!
//! Every other limit here spoils one message. These four take the whole
//! bot's command surface out of a guild at once.
//!
//! `GuildId::set_commands` sends every command in one payload, so one
//! description a character too long is a 400 for all of them.
//! [`crate::bot::command::register`] prints one line to stderr and
//! carries on; the gateway stays up, log streaming and the monitor keep
//! working, and nothing looks broken. Meanwhile `/shep`, `/system` and
//! `/monitor` have all silently vanished from the guild, and one
//! over-long option description on one subcommand is enough to do it.
//!
//! So these are asserted rather than fitted, which is the opposite of
//! every other cap in this file. A truncated sheep name is a cosmetic
//! loss in a message somebody is reading; a truncated command
//! description is a permanent lie in Discord's own UI about what a
//! command does. These strings are written by whoever edits this crate,
//! not supplied by an operator, so the right place to catch one is a
//! failing test before it ships.
//!
//! `custom_id`'s own 100 character cap stays where it is, in
//! [`crate::bot::embed`]: nothing there needs truncating, since
//! [`crate::bot::embed::custom_id`] builds its id from a fixed short word
//! and a `u32`, never from an unbounded string. This module is for a limit
//! that a real, operator-supplied string can actually exceed.
/// The longest an embed's `title` may be. Discord's own limit.
pub const EMBED_TITLE_LIMIT: usize = 256;
/// The longest an embed's `description` may be. Discord's own limit.
pub const EMBED_DESCRIPTION_LIMIT: usize = 4096;
/// The longest an embed's field `value` this crate lets through, in
/// characters.
///
/// Discord's own ceiling on a field value is 1,024, and this crate keeps
/// that ceiling rather than lowering it: see
/// [`crate::bot::embed`]'s module doc for the arithmetic proving every
/// field [`crate::bot::embed::process_embed`] can build, all present at
/// once and each at this cap, still sums under [`MESSAGE_CHARACTER_BUDGET`]
/// with room to spare. A lower cap is only needed when the arithmetic
/// says so, and here it does not.
pub const FIELD_VALUE_LIMIT: usize = 1024;
/// The character budget for everything counted, summed across every embed
/// on one message: every `title`, `description`, `field.name`,
/// `field.value`, `footer.text` and `author.name`. Discord's own limit.
pub const MESSAGE_CHARACTER_BUDGET: usize = 6000;
/// The most embeds one message may carry. Discord's own limit, and a count
/// rather than a length, but it belongs beside [`MESSAGE_CHARACTER_BUDGET`]
/// for the same reason: a packer that only watches the character sum can
/// still stack eleven small embeds onto one message and take a 400 for the
/// count alone, the same shape of gap a length-only check would leave.
pub const EMBED_MAX_COUNT: usize = 10;
/// The longest an autocomplete choice's `name` (and, since serenity's
/// `AutocompleteChoice::from` sets both from the same string, its `value`
/// too) may be. Discord's own limit, and the ninth this port has run
/// into.
///
/// Not fed to [`fit`]: a truncated `value` is what Discord sends back as
/// the argument once an operator picks the suggestion, so a truncated
/// name would offer a sheep that does not exist under that shortened
/// name, and the verb would fail against it. A name that cannot be sent
/// whole cannot be offered as a working suggestion at all, so
/// [`crate::bot::commands::shep::ShepCommand::suggestions`] drops it
/// instead of shortening it, keeping every other suggestion in the same
/// response alive. Discord answers one autocomplete response with every
/// suggestion in a single payload, so a single over-length name, if sent,
/// would fail the whole response and silently empty every suggestion for
/// that keystroke, not just the one that was too long.
pub const AUTOCOMPLETE_CHOICE_NAME_LIMIT: usize = 100;
/// The longest a slash command's `name` may be. Discord's own limit, and
/// the same cap an option's name carries; see [`OPTION_NAME_LIMIT`] for
/// why the two are named separately anyway.
pub const COMMAND_NAME_LIMIT: usize = 32;
/// The longest a slash command's `description` may be. Discord's own
/// limit.
pub const COMMAND_DESCRIPTION_LIMIT: usize = 100;
/// The longest a command option's `name` may be, at any nesting depth: a
/// subcommand's own name and the name of an option under it are both
/// options as far as Discord's payload is concerned.
///
/// Numerically the same as [`COMMAND_NAME_LIMIT`] and named separately on
/// purpose. They are two limits that happen to agree, not one limit used
/// twice, and a reader checking one of them against Discord's
/// documentation should not have to work out which.
pub const OPTION_NAME_LIMIT: usize = 32;
/// The longest a command option's `description` may be, at any nesting
/// depth. Discord's own limit, and the one this crate comes closest to:
/// the longest description in the tree today is 70 characters.
pub const OPTION_DESCRIPTION_LIMIT: usize = 100;
/// The longest a message's own `content` field may be, separate from
/// anything an embed carries. Discord's own limit, and the sixth of its
/// length limits this port has run into: an error's `Display` is
/// interpolated straight into a followup's `content` in
/// `crate::bot::interaction::report_failure`, with no bound of its own
/// before this cap was named here.
pub const MESSAGE_CONTENT_LIMIT: usize = 2000;
/// Truncate `text` to at most `limit` characters, ending in a single
/// `'\u{2026}'` ellipsis when it had to cut anything.
///
/// Counts characters, not bytes: Discord counts characters for every one of
/// the limits above, and a byte-based split can cut a multibyte character
/// in half or refuse text Discord would have accepted whole. Text already
/// at or under `limit` comes back unchanged; text over it comes back at
/// exactly `limit` characters, the last of them the ellipsis, so a caller
/// can always trust `fit(text, limit).chars().count() <= limit`. This is
/// the same shape `shep-cli`'s own `lookout::view::flock::layout::fit`
/// uses to fit a name to a terminal column, so a truncated string in this
/// crate reads the same way a truncated one does in shep's other clients.
///
/// `limit == 0` is the one degenerate case: there is no room for even the
/// ellipsis, so this returns an empty string rather than one character
/// over budget.