mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-26 19:56:37 +08:00
fix(discord): page the inbounds reply within Discord's embed caps (#6496)
`!inbounds` built a single embed with one field per inbound and sent it as
it was. Discord rejects the whole message past 25 fields, ten embeds or 6000
counted characters, so an operator holding more than 25 inbounds got no
answer at all, and a remark longer than ~252 runes broke the command on its
own — the `📍 ` prefix spends four units of the same 256-unit field name cap.
The failure left no trace either: the send error was discarded, so the
channel stayed empty and the log stayed quiet.
Fields are now capped by the same helper every other reply in the package
uses for its name and value limits, and packed into messages that fit those
caps, with the header leading only the first embed of each message. The caps
are counted the way Discord counts them, in UTF-16 units, and a page it
answers with a 429 is waited out once rather than dropping the pages behind
it.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
@@ -119,6 +120,22 @@ func (s *DiscordService) authCredentials() (token string, channelID string, err
|
||||
return cleanToken, strings.TrimSpace(rawChannel), nil
|
||||
}
|
||||
|
||||
// discordCharLen counts what Discord's caps count: a rune outside the BMP is
|
||||
// two units there, so a rune count understates an emoji-bearing field.
|
||||
func discordCharLen(s string) int {
|
||||
return len(utf16.Encode([]rune(s)))
|
||||
}
|
||||
|
||||
// RateLimitedError is a 429, carrying the wait Discord asks for so a caller
|
||||
// sending several messages can back off instead of losing the rest of them.
|
||||
type RateLimitedError struct {
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
func (e *RateLimitedError) Error() string {
|
||||
return fmt.Sprintf("discord rate limited (429): retry after %s", e.RetryAfter)
|
||||
}
|
||||
|
||||
func parseDiscordResponse(resp *http.Response) error {
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
bodyStr := string(respBody)
|
||||
@@ -135,7 +152,11 @@ func parseDiscordResponse(resp *http.Response) error {
|
||||
case http.StatusNotFound:
|
||||
return errors.New("discord not found (404): channel not found")
|
||||
case http.StatusTooManyRequests:
|
||||
return fmt.Errorf("discord rate limited (429): %s", bodyStr)
|
||||
var limited struct {
|
||||
RetryAfter float64 `json:"retry_after"`
|
||||
}
|
||||
_ = json.Unmarshal(respBody, &limited)
|
||||
return &RateLimitedError{RetryAfter: time.Duration(limited.RetryAfter * float64(time.Second))}
|
||||
default:
|
||||
return fmt.Errorf("discord API error (%d): %s", resp.StatusCode, bodyStr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user