(new): PollTimeout config, RateLimiter.Cleanup
Golang lint / lint (pull_request) Successful in 3m4s
Golang lint / lint (push) Successful in 3m5s

(fix): compact payload escape, Draft.push validation, plugin logger ownership, worker StopAndWait, getChatLimiter deadlock, runner ctx-after-tick
(refactor): remove NewPayload, buildSceneKey from sceneRuntime, unify ToJSON fallback
(tests): compact round-trip, Draft.push state, RateLimiter.Cleanup eviction
(doc): changelog v1.0.0 rewrite, NewCommand dual-use godoc
This commit is contained in:
2026-05-18 17:27:14 +03:00
parent 09fb9261df
commit affb802a7b
38 changed files with 697 additions and 345 deletions
+84 -11
View File
@@ -26,7 +26,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
msgCtx := &MsgContext{
msgCtx := &MessageContext{
Update: *u, API: bot.api,
Logger: bot.logger,
errorTemplate: bot.errorTemplate,
@@ -35,6 +35,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
sceneRuntime: bot,
observer: bot.observer,
payloadType: bot.payloadType,
botID: bot.userID,
ctx: ctx,
}
bot.prepareUpdateCtx(u, msgCtx)
@@ -114,7 +115,7 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
})
}
func cloneMsgContext(src *MsgContext) *MsgContext {
func cloneMsgContext(src *MessageContext) *MessageContext {
cloned := *src
if src.Args != nil {
cloned.Args = append([]string(nil), src.Args...)
@@ -154,20 +155,92 @@ func decodeBase64Payload(s string) (CallbackData, error) {
return decodeJSONPayload(string(b))
}
func encodeCompactPayload(d CallbackData) (string, error) {
args := strings.Join(d.Args, ",")
return d.Command + "|" + args, nil
// Compact payload format: cmd|arg1,arg2,...
// Bytes \, |, and , inside a part are escaped with a leading backslash so the
// payload round-trips without ambiguity. Encoding/decoding operate byte-wise
// because all separators are single-byte ASCII; multi-byte UTF-8 code points
// pass through unchanged.
func encodeCompactPart(s string) string {
if !strings.ContainsAny(s, `\|,`) {
return s
}
var b strings.Builder
b.Grow(len(s) + 2)
for i := 0; i < len(s); i++ {
switch s[i] {
case '\\', '|', ',':
b.WriteByte('\\')
}
b.WriteByte(s[i])
}
return b.String()
}
func decodeCompactPart(s string) string {
if !strings.Contains(s, `\`) {
return s
}
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) {
b.WriteByte(s[i+1])
i++
continue
}
b.WriteByte(s[i])
}
return b.String()
}
func encodeCompactPayload(d CallbackData) (string, error) {
var b strings.Builder
b.WriteString(encodeCompactPart(d.Command))
b.WriteByte('|')
for i, a := range d.Args {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(encodeCompactPart(a))
}
return b.String(), nil
}
func decodeCompactPayload(s string) (CallbackData, error) {
values := strings.SplitN(s, "|", 2)
if len(values) != 2 {
sepIdx := -1
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) {
i++
continue
}
if s[i] == '|' {
sepIdx = i
break
}
}
if sepIdx == -1 {
return CallbackData{}, errors.New("invalid payload")
}
cmd, argsRaw := values[0], values[1]
var args []string
if argsRaw != "" {
args = strings.Split(argsRaw, ",")
cmd := decodeCompactPart(s[:sepIdx])
argsRaw := s[sepIdx+1:]
if argsRaw == "" {
return CallbackData{Command: cmd}, nil
}
var args []string
start := 0
for i := 0; i < len(argsRaw); i++ {
if argsRaw[i] == '\\' && i+1 < len(argsRaw) {
i++
continue
}
if argsRaw[i] == ',' {
args = append(args, decodeCompactPart(argsRaw[start:i]))
start = i + 1
}
}
args = append(args, decodeCompactPart(argsRaw[start:]))
return CallbackData{Command: cmd, Args: args}, nil
}
func encodeCompactBase64Payload(d CallbackData) (string, error) {