mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
refactor(telegram): reduce code duplication and improve readability
- Extract parseTelegramTarget helper to consolidate duplicated @username vs numeric chat ID parsing from 6+ locations (builder functions, sendTelegramTextReturnMessage, sendTelegramAttachmentImpl) - Extract Config.baseURL() to eliminate duplicate base URL resolution between apiEndpoint() and fileEndpoint() - Refactor stream.go Push method: extract resetStreamState(), deliverFinalText(), and per-event-type sub-methods (pushDelta, pushFinal, pushToolCallStart, pushAttachment, pushPhaseEnd, pushError), reducing the 200-line switch-case to a clean dispatcher - Use pushFinal's existing getBot() instead of duplicating parseConfig + getOrCreateBot - Replace sort.SliceStable with slices.SortStableFunc + cmp.Compare - Replace strings.Index + manual slicing with strings.Cut in decodeDataURLBytes, ResolveAttachment, and parseTelegramUserInput
This commit is contained in:
@@ -15,22 +15,23 @@ type Config struct {
|
|||||||
APIBaseURL string // Reverse proxy base URL for regions where Telegram is blocked (e.g. China mainland)
|
APIBaseURL string // Reverse proxy base URL for regions where Telegram is blocked (e.g. China mainland)
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiEndpoint returns the Sprintf-formatted API endpoint derived from the base URL.
|
// baseURL returns the effective base URL with trailing slashes removed.
|
||||||
func (c Config) apiEndpoint() string {
|
func (c Config) baseURL() string {
|
||||||
base := c.APIBaseURL
|
base := c.APIBaseURL
|
||||||
if base == "" {
|
if base == "" {
|
||||||
base = defaultAPIBaseURL
|
base = defaultAPIBaseURL
|
||||||
}
|
}
|
||||||
return strings.TrimRight(base, "/") + "/bot%s/%s"
|
return strings.TrimRight(base, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiEndpoint returns the Sprintf-formatted API endpoint derived from the base URL.
|
||||||
|
func (c Config) apiEndpoint() string {
|
||||||
|
return c.baseURL() + "/bot%s/%s"
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileEndpoint returns the Sprintf-formatted file download endpoint derived from the base URL.
|
// fileEndpoint returns the Sprintf-formatted file download endpoint derived from the base URL.
|
||||||
func (c Config) fileEndpoint() string {
|
func (c Config) fileEndpoint() string {
|
||||||
base := c.APIBaseURL
|
return c.baseURL() + "/file/bot%s/%s"
|
||||||
if base == "" {
|
|
||||||
base = defaultAPIBaseURL
|
|
||||||
}
|
|
||||||
return strings.TrimRight(base, "/") + "/file/bot%s/%s"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserConfig holds the identifiers used to target a Telegram user or group.
|
// UserConfig holds the identifiers used to target a Telegram user or group.
|
||||||
|
|||||||
@@ -222,11 +222,9 @@ func parseTelegramUserInput(input string) (chatID, userID int64) {
|
|||||||
if input == "" {
|
if input == "" {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
if idx := strings.Index(input, ":"); idx >= 0 {
|
if left, right, ok := strings.Cut(input, ":"); ok {
|
||||||
left := strings.TrimSpace(input[:idx])
|
cid, err1 := strconv.ParseInt(strings.TrimSpace(left), 10, 64)
|
||||||
right := strings.TrimSpace(input[idx+1:])
|
uid, err2 := strconv.ParseInt(strings.TrimSpace(right), 10, 64)
|
||||||
cid, err1 := strconv.ParseInt(left, 10, 64)
|
|
||||||
uid, err2 := strconv.ParseInt(right, 10, 64)
|
|
||||||
if err1 == nil && err2 == nil && cid != 0 && uid != 0 {
|
if err1 == nil && err2 == nil && cid != 0 && uid != 0 {
|
||||||
return cid, uid
|
return cid, uid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,188 @@ func (s *telegramOutboundStream) sendPermanentMessage(ctx context.Context, text
|
|||||||
return sendTelegramText(bot, s.target, text, replyTo, parseMode)
|
return sendTelegramText(bot, s.target, text, replyTo, parseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetStreamState clears the streaming message state so a fresh message will
|
||||||
|
// be created on the next delta. Must be called without holding s.mu.
|
||||||
|
func (s *telegramOutboundStream) resetStreamState() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.streamMsgID = 0
|
||||||
|
if !s.isPrivateChat {
|
||||||
|
s.streamChatID = 0
|
||||||
|
}
|
||||||
|
s.lastEdited = ""
|
||||||
|
s.lastEditedAt = time.Time{}
|
||||||
|
s.buf.Reset()
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// deliverFinalText sends or edits the final text depending on chat mode.
|
||||||
|
func (s *telegramOutboundStream) deliverFinalText(ctx context.Context, text, parseMode string) error {
|
||||||
|
if s.isPrivateChat {
|
||||||
|
return s.sendPermanentMessage(ctx, text, parseMode)
|
||||||
|
}
|
||||||
|
if err := s.ensureStreamMessage(ctx, text); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.editStreamMessageFinal(ctx, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushToolCallStart(ctx context.Context) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
bufText := strings.TrimSpace(s.buf.String())
|
||||||
|
hasMsg := s.streamMsgID != 0
|
||||||
|
s.mu.Unlock()
|
||||||
|
if s.isPrivateChat {
|
||||||
|
// In draft mode, send buffered text as a permanent message before tool execution.
|
||||||
|
if bufText != "" {
|
||||||
|
if err := s.sendPermanentMessage(ctx, bufText, ""); err != nil {
|
||||||
|
if s.adapter != nil && s.adapter.logger != nil {
|
||||||
|
s.adapter.logger.Warn("telegram: draft permanent message failed", slog.Any("error", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if hasMsg && bufText != "" {
|
||||||
|
_ = s.editStreamMessageFinal(ctx, bufText)
|
||||||
|
}
|
||||||
|
s.resetStreamState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushAttachment(ctx context.Context, event channel.StreamEvent) error {
|
||||||
|
if len(event.Attachments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bot, replyTo, err := s.getBotAndReply(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, att := range event.Attachments {
|
||||||
|
if sendErr := sendTelegramAttachmentWithAssets(ctx, bot, s.target, att, "", replyTo, "", s.adapter.assets); sendErr != nil {
|
||||||
|
if s.adapter != nil && s.adapter.logger != nil {
|
||||||
|
s.adapter.logger.Warn("telegram: stream attachment send failed",
|
||||||
|
slog.String("config_id", s.cfg.ID),
|
||||||
|
slog.String("type", string(att.Type)),
|
||||||
|
slog.Any("error", sendErr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushPhaseEnd(ctx context.Context, event channel.StreamEvent) error {
|
||||||
|
if event.Phase != channel.StreamPhaseText {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// In draft mode, skip phase-end finalization; StreamEventFinal sends the
|
||||||
|
// permanent formatted message.
|
||||||
|
if s.isPrivateChat {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
finalText := strings.TrimSpace(s.buf.String())
|
||||||
|
s.mu.Unlock()
|
||||||
|
if finalText != "" {
|
||||||
|
if err := s.ensureStreamMessage(ctx, finalText); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.editStreamMessageFinal(ctx, finalText)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushDelta(ctx context.Context, event channel.StreamEvent) error {
|
||||||
|
if event.Delta == "" || event.Phase == channel.StreamPhaseReasoning {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
s.buf.WriteString(event.Delta)
|
||||||
|
content := s.buf.String()
|
||||||
|
s.mu.Unlock()
|
||||||
|
if s.isPrivateChat {
|
||||||
|
return s.sendDraft(ctx, content)
|
||||||
|
}
|
||||||
|
if err := s.ensureStreamMessage(ctx, content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.editStreamMessage(ctx, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushFinal(ctx context.Context, event channel.StreamEvent) error {
|
||||||
|
// In draft mode, read and reset buffer atomically to prevent duplicate
|
||||||
|
// permanent messages when multiple StreamEventFinal events fire
|
||||||
|
// (one per assistant output in multi-tool-call responses).
|
||||||
|
s.mu.Lock()
|
||||||
|
bufText := strings.TrimSpace(s.buf.String())
|
||||||
|
if s.isPrivateChat {
|
||||||
|
s.buf.Reset()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if event.Final == nil || event.Final.Message.IsEmpty() {
|
||||||
|
if bufText != "" {
|
||||||
|
if err := s.deliverFinalText(ctx, bufText, ""); err != nil {
|
||||||
|
if s.adapter != nil && s.adapter.logger != nil {
|
||||||
|
s.adapter.logger.Warn("telegram: deliver buffered final text failed", slog.Any("error", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := event.Final.Message
|
||||||
|
finalText := bufText
|
||||||
|
if finalText == "" && !s.isPrivateChat {
|
||||||
|
finalText = strings.TrimSpace(msg.PlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert markdown to Telegram HTML for the final message.
|
||||||
|
formatted, pm := formatTelegramOutput(finalText, msg.Format)
|
||||||
|
if pm != "" {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.parseMode = pm
|
||||||
|
s.mu.Unlock()
|
||||||
|
finalText = formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.deliverFinalText(ctx, finalText, s.parseMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.Attachments) > 0 {
|
||||||
|
bot, err := s.getBot(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
replyTo := parseReplyToMessageID(s.reply)
|
||||||
|
parseMode := resolveTelegramParseMode(msg.Format)
|
||||||
|
for i, att := range msg.Attachments {
|
||||||
|
to := replyTo
|
||||||
|
if i > 0 {
|
||||||
|
to = 0
|
||||||
|
}
|
||||||
|
if err := sendTelegramAttachmentWithAssets(ctx, bot, s.target, att, "", to, parseMode, s.adapter.assets); err != nil && s.adapter.logger != nil {
|
||||||
|
s.adapter.logger.Error("stream final attachment failed", slog.String("config_id", s.cfg.ID), slog.Any("error", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramOutboundStream) pushError(ctx context.Context, event channel.StreamEvent) error {
|
||||||
|
errText := strings.TrimSpace(event.Error)
|
||||||
|
if errText == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
display := "Error: " + errText
|
||||||
|
if s.isPrivateChat {
|
||||||
|
return s.sendPermanentMessage(ctx, display, "")
|
||||||
|
}
|
||||||
|
if err := s.ensureStreamMessage(ctx, display); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.editStreamMessage(ctx, display)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *telegramOutboundStream) Push(ctx context.Context, event channel.StreamEvent) error {
|
func (s *telegramOutboundStream) Push(ctx context.Context, event channel.StreamEvent) error {
|
||||||
if s == nil || s.adapter == nil {
|
if s == nil || s.adapter == nil {
|
||||||
return errors.New("telegram stream not configured")
|
return errors.New("telegram stream not configured")
|
||||||
@@ -278,196 +460,21 @@ func (s *telegramOutboundStream) Push(ctx context.Context, event channel.StreamE
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case channel.StreamEventStatus:
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventToolCallStart:
|
case channel.StreamEventToolCallStart:
|
||||||
s.mu.Lock()
|
return s.pushToolCallStart(ctx)
|
||||||
bufText := strings.TrimSpace(s.buf.String())
|
|
||||||
hasMsg := s.streamMsgID != 0
|
|
||||||
s.mu.Unlock()
|
|
||||||
if s.isPrivateChat {
|
|
||||||
// In draft mode, send buffered text as a permanent message before tool execution.
|
|
||||||
if bufText != "" {
|
|
||||||
if err := s.sendPermanentMessage(ctx, bufText, ""); err != nil {
|
|
||||||
if s.adapter != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Warn("telegram: draft permanent message failed", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if hasMsg && bufText != "" {
|
|
||||||
_ = s.editStreamMessageFinal(ctx, bufText)
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
s.streamMsgID = 0
|
|
||||||
if !s.isPrivateChat {
|
|
||||||
s.streamChatID = 0
|
|
||||||
}
|
|
||||||
s.lastEdited = ""
|
|
||||||
s.lastEditedAt = time.Time{}
|
|
||||||
s.buf.Reset()
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventToolCallEnd:
|
case channel.StreamEventToolCallEnd:
|
||||||
s.mu.Lock()
|
s.resetStreamState()
|
||||||
s.streamMsgID = 0
|
|
||||||
if !s.isPrivateChat {
|
|
||||||
s.streamChatID = 0
|
|
||||||
}
|
|
||||||
s.lastEdited = ""
|
|
||||||
s.lastEditedAt = time.Time{}
|
|
||||||
s.buf.Reset()
|
|
||||||
s.mu.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
case channel.StreamEventAttachment:
|
case channel.StreamEventAttachment:
|
||||||
if len(event.Attachments) == 0 {
|
return s.pushAttachment(ctx, event)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
bot, replyTo, err := s.getBotAndReply(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, att := range event.Attachments {
|
|
||||||
if sendErr := sendTelegramAttachmentWithAssets(ctx, bot, s.target, att, "", replyTo, "", s.adapter.assets); sendErr != nil {
|
|
||||||
if s.adapter != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Warn("telegram: stream attachment send failed",
|
|
||||||
slog.String("config_id", s.cfg.ID),
|
|
||||||
slog.String("type", string(att.Type)),
|
|
||||||
slog.Any("error", sendErr),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventPhaseStart:
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventPhaseEnd:
|
case channel.StreamEventPhaseEnd:
|
||||||
if event.Phase == channel.StreamPhaseText {
|
return s.pushPhaseEnd(ctx, event)
|
||||||
// In draft mode, skip phase-end finalization; StreamEventFinal sends the
|
|
||||||
// permanent formatted message.
|
|
||||||
if s.isPrivateChat {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
finalText := strings.TrimSpace(s.buf.String())
|
|
||||||
s.mu.Unlock()
|
|
||||||
if finalText != "" {
|
|
||||||
if err := s.ensureStreamMessage(ctx, finalText); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.editStreamMessageFinal(ctx, finalText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventProcessingFailed, channel.StreamEventAgentStart, channel.StreamEventAgentEnd, channel.StreamEventProcessingStarted, channel.StreamEventProcessingCompleted:
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventDelta:
|
case channel.StreamEventDelta:
|
||||||
if event.Delta == "" || event.Phase == channel.StreamPhaseReasoning {
|
return s.pushDelta(ctx, event)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
s.buf.WriteString(event.Delta)
|
|
||||||
content := s.buf.String()
|
|
||||||
s.mu.Unlock()
|
|
||||||
if s.isPrivateChat {
|
|
||||||
return s.sendDraft(ctx, content)
|
|
||||||
}
|
|
||||||
if err := s.ensureStreamMessage(ctx, content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.editStreamMessage(ctx, content)
|
|
||||||
case channel.StreamEventFinal:
|
case channel.StreamEventFinal:
|
||||||
// In draft mode, read and reset buffer atomically to prevent duplicate
|
return s.pushFinal(ctx, event)
|
||||||
// permanent messages when multiple StreamEventFinal events fire
|
|
||||||
// (one per assistant output in multi-tool-call responses).
|
|
||||||
s.mu.Lock()
|
|
||||||
bufText := strings.TrimSpace(s.buf.String())
|
|
||||||
if s.isPrivateChat {
|
|
||||||
s.buf.Reset()
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
if event.Final == nil || event.Final.Message.IsEmpty() {
|
|
||||||
if bufText != "" {
|
|
||||||
if s.isPrivateChat {
|
|
||||||
if err := s.sendPermanentMessage(ctx, bufText, ""); err != nil {
|
|
||||||
if s.adapter != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Warn("telegram: draft final permanent message failed", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := s.ensureStreamMessage(ctx, bufText); err != nil {
|
|
||||||
if s.adapter != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Warn("telegram: ensure stream message failed", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.editStreamMessageFinal(ctx, bufText); err != nil {
|
|
||||||
if s.adapter != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Warn("telegram: edit stream message failed", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg := event.Final.Message
|
|
||||||
finalText := bufText
|
|
||||||
if finalText == "" && !s.isPrivateChat {
|
|
||||||
finalText = strings.TrimSpace(msg.PlainText())
|
|
||||||
}
|
|
||||||
// Convert markdown to Telegram HTML for the final message.
|
|
||||||
formatted, pm := formatTelegramOutput(finalText, msg.Format)
|
|
||||||
if pm != "" {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.parseMode = pm
|
|
||||||
s.mu.Unlock()
|
|
||||||
finalText = formatted
|
|
||||||
}
|
|
||||||
if s.isPrivateChat {
|
|
||||||
if err := s.sendPermanentMessage(ctx, finalText, s.parseMode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := s.ensureStreamMessage(ctx, finalText); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.editStreamMessageFinal(ctx, finalText); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(msg.Attachments) > 0 {
|
|
||||||
replyTo := parseReplyToMessageID(s.reply)
|
|
||||||
telegramCfg, err := parseConfig(s.cfg.Credentials)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bot, err := s.adapter.getOrCreateBot(telegramCfg, s.cfg.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
parseMode := resolveTelegramParseMode(msg.Format)
|
|
||||||
for i, att := range msg.Attachments {
|
|
||||||
to := replyTo
|
|
||||||
if i > 0 {
|
|
||||||
to = 0
|
|
||||||
}
|
|
||||||
if err := sendTelegramAttachmentWithAssets(ctx, bot, s.target, att, "", to, parseMode, s.adapter.assets); err != nil && s.adapter.logger != nil {
|
|
||||||
s.adapter.logger.Error("stream final attachment failed", slog.String("config_id", s.cfg.ID), slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case channel.StreamEventError:
|
case channel.StreamEventError:
|
||||||
errText := strings.TrimSpace(event.Error)
|
return s.pushError(ctx, event)
|
||||||
if errText == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
display := "Error: " + errText
|
|
||||||
if s.isPrivateChat {
|
|
||||||
return s.sendPermanentMessage(ctx, display, "")
|
|
||||||
}
|
|
||||||
if err := s.ensureStreamMessage(ctx, display); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.editStreamMessage(ctx, display)
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -8,7 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -434,8 +435,8 @@ func (a *TelegramAdapter) buildTelegramMediaGroupInboundMessage(
|
|||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return channel.InboundMessage{}, false
|
return channel.InboundMessage{}, false
|
||||||
}
|
}
|
||||||
sort.SliceStable(items, func(i, j int) bool {
|
slices.SortStableFunc(items, func(a, b *tgbotapi.Message) int {
|
||||||
return items[i].MessageID < items[j].MessageID
|
return cmp.Compare(a.MessageID, b.MessageID)
|
||||||
})
|
})
|
||||||
anchor := items[0]
|
anchor := items[0]
|
||||||
text := ""
|
text := ""
|
||||||
@@ -743,32 +744,25 @@ func sendTelegramTextReturnMessage(bot *tgbotapi.BotAPI, target string, text str
|
|||||||
if sendTextForTest != nil {
|
if sendTextForTest != nil {
|
||||||
return sendTextForTest(bot, target, text, replyTo, parseMode)
|
return sendTextForTest(bot, target, text, replyTo, parseMode)
|
||||||
}
|
}
|
||||||
var sent tgbotapi.Message
|
parsedChatID, channelUsername, parseErr := parseTelegramTarget(target)
|
||||||
if strings.HasPrefix(target, "@") {
|
if parseErr != nil {
|
||||||
message := tgbotapi.NewMessageToChannel(target, text)
|
return 0, 0, parseErr
|
||||||
message.ParseMode = parseMode
|
|
||||||
if replyTo > 0 {
|
|
||||||
message.ReplyToMessageID = replyTo
|
|
||||||
}
|
|
||||||
sent, err = bot.Send(message)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chatID, err = strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, errors.New("telegram target must be @username or chat_id")
|
|
||||||
}
|
|
||||||
message := tgbotapi.NewMessage(chatID, text)
|
|
||||||
message.ParseMode = parseMode
|
|
||||||
if replyTo > 0 {
|
|
||||||
message.ReplyToMessageID = replyTo
|
|
||||||
}
|
|
||||||
sent, err = bot.Send(message)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var message tgbotapi.MessageConfig
|
||||||
|
if channelUsername != "" {
|
||||||
|
message = tgbotapi.NewMessageToChannel(channelUsername, text)
|
||||||
|
} else {
|
||||||
|
message = tgbotapi.NewMessage(parsedChatID, text)
|
||||||
|
}
|
||||||
|
message.ParseMode = parseMode
|
||||||
|
if replyTo > 0 {
|
||||||
|
message.ReplyToMessageID = replyTo
|
||||||
|
}
|
||||||
|
sent, err := bot.Send(message)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
chatID = parsedChatID
|
||||||
if sent.Chat != nil {
|
if sent.Chat != nil {
|
||||||
chatID = sent.Chat.ID
|
chatID = sent.Chat.ID
|
||||||
}
|
}
|
||||||
@@ -875,17 +869,17 @@ func sendTelegramAttachmentImpl(ctx context.Context, bot *tgbotapi.BotAPI, targe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
isChannel := strings.HasPrefix(target, "@")
|
chatID, channelUsername, targetErr := parseTelegramTarget(target)
|
||||||
|
if targetErr != nil {
|
||||||
|
return targetErr
|
||||||
|
}
|
||||||
|
isChannel := channelUsername != ""
|
||||||
switch att.Type {
|
switch att.Type {
|
||||||
case channel.AttachmentImage:
|
case channel.AttachmentImage:
|
||||||
var photo tgbotapi.PhotoConfig
|
var photo tgbotapi.PhotoConfig
|
||||||
if isChannel {
|
if isChannel {
|
||||||
photo = tgbotapi.NewPhotoToChannel(target, file)
|
photo = tgbotapi.NewPhotoToChannel(channelUsername, file)
|
||||||
} else {
|
} else {
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("telegram target must be @username or chat_id")
|
|
||||||
}
|
|
||||||
photo = tgbotapi.NewPhoto(chatID, file)
|
photo = tgbotapi.NewPhoto(chatID, file)
|
||||||
}
|
}
|
||||||
photo.Caption = caption
|
photo.Caption = caption
|
||||||
@@ -900,15 +894,11 @@ func sendTelegramAttachmentImpl(ctx context.Context, bot *tgbotapi.BotAPI, targe
|
|||||||
if isChannel {
|
if isChannel {
|
||||||
document = tgbotapi.DocumentConfig{
|
document = tgbotapi.DocumentConfig{
|
||||||
BaseFile: tgbotapi.BaseFile{
|
BaseFile: tgbotapi.BaseFile{
|
||||||
BaseChat: tgbotapi.BaseChat{ChannelUsername: target},
|
BaseChat: tgbotapi.BaseChat{ChannelUsername: channelUsername},
|
||||||
File: file,
|
File: file,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("telegram target must be @username or chat_id")
|
|
||||||
}
|
|
||||||
document = tgbotapi.NewDocument(chatID, file)
|
document = tgbotapi.NewDocument(chatID, file)
|
||||||
}
|
}
|
||||||
document.Caption = caption
|
document.Caption = caption
|
||||||
@@ -1017,8 +1007,8 @@ func resolveTelegramFile(ctx context.Context, urlRef, keyRef, base64Ref, sourceP
|
|||||||
|
|
||||||
func decodeDataURLBytes(dataURL string) ([]byte, error) {
|
func decodeDataURLBytes(dataURL string) ([]byte, error) {
|
||||||
value := dataURL
|
value := dataURL
|
||||||
if idx := strings.Index(value, ","); idx >= 0 {
|
if _, after, ok := strings.Cut(value, ","); ok {
|
||||||
value = value[idx+1:]
|
value = after
|
||||||
}
|
}
|
||||||
return io.ReadAll(io.LimitReader(
|
return io.ReadAll(io.LimitReader(
|
||||||
base64StdDecoder(strings.NewReader(value)),
|
base64StdDecoder(strings.NewReader(value)),
|
||||||
@@ -1155,56 +1145,58 @@ func buildTelegramForwardContext(msg *tgbotapi.Message) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTelegramAudio(target string, file tgbotapi.RequestFileData) (tgbotapi.AudioConfig, error) {
|
// parseTelegramTarget resolves a target string into a numeric chat ID and an
|
||||||
|
// optional channel username. Exactly one of chatID or channelUsername will be
|
||||||
|
// set; callers can use this to construct any message config type.
|
||||||
|
func parseTelegramTarget(target string) (chatID int64, channelUsername string, err error) {
|
||||||
if strings.HasPrefix(target, "@") {
|
if strings.HasPrefix(target, "@") {
|
||||||
audio := tgbotapi.NewAudio(0, file)
|
return 0, target, nil
|
||||||
audio.ChannelUsername = target
|
|
||||||
return audio, nil
|
|
||||||
}
|
}
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
chatID, err = strconv.ParseInt(target, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tgbotapi.AudioConfig{}, errors.New("telegram target must be @username or chat_id")
|
return 0, "", errors.New("telegram target must be @username or chat_id")
|
||||||
}
|
}
|
||||||
return tgbotapi.NewAudio(chatID, file), nil
|
return chatID, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTelegramAudio(target string, file tgbotapi.RequestFileData) (tgbotapi.AudioConfig, error) {
|
||||||
|
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||||
|
if err != nil {
|
||||||
|
return tgbotapi.AudioConfig{}, err
|
||||||
|
}
|
||||||
|
audio := tgbotapi.NewAudio(chatID, file)
|
||||||
|
audio.ChannelUsername = channelUsername
|
||||||
|
return audio, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTelegramVoice(target string, file tgbotapi.RequestFileData) (tgbotapi.VoiceConfig, error) {
|
func buildTelegramVoice(target string, file tgbotapi.RequestFileData) (tgbotapi.VoiceConfig, error) {
|
||||||
if strings.HasPrefix(target, "@") {
|
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||||
voice := tgbotapi.NewVoice(0, file)
|
|
||||||
voice.ChannelUsername = target
|
|
||||||
return voice, nil
|
|
||||||
}
|
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tgbotapi.VoiceConfig{}, errors.New("telegram target must be @username or chat_id")
|
return tgbotapi.VoiceConfig{}, err
|
||||||
}
|
}
|
||||||
return tgbotapi.NewVoice(chatID, file), nil
|
voice := tgbotapi.NewVoice(chatID, file)
|
||||||
|
voice.ChannelUsername = channelUsername
|
||||||
|
return voice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTelegramVideo(target string, file tgbotapi.RequestFileData) (tgbotapi.VideoConfig, error) {
|
func buildTelegramVideo(target string, file tgbotapi.RequestFileData) (tgbotapi.VideoConfig, error) {
|
||||||
if strings.HasPrefix(target, "@") {
|
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||||
video := tgbotapi.NewVideo(0, file)
|
|
||||||
video.ChannelUsername = target
|
|
||||||
return video, nil
|
|
||||||
}
|
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tgbotapi.VideoConfig{}, errors.New("telegram target must be @username or chat_id")
|
return tgbotapi.VideoConfig{}, err
|
||||||
}
|
}
|
||||||
return tgbotapi.NewVideo(chatID, file), nil
|
video := tgbotapi.NewVideo(chatID, file)
|
||||||
|
video.ChannelUsername = channelUsername
|
||||||
|
return video, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTelegramAnimation(target string, file tgbotapi.RequestFileData) (tgbotapi.AnimationConfig, error) {
|
func buildTelegramAnimation(target string, file tgbotapi.RequestFileData) (tgbotapi.AnimationConfig, error) {
|
||||||
if strings.HasPrefix(target, "@") {
|
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||||
animation := tgbotapi.NewAnimation(0, file)
|
|
||||||
animation.ChannelUsername = target
|
|
||||||
return animation, nil
|
|
||||||
}
|
|
||||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tgbotapi.AnimationConfig{}, errors.New("telegram target must be @username or chat_id")
|
return tgbotapi.AnimationConfig{}, err
|
||||||
}
|
}
|
||||||
return tgbotapi.NewAnimation(chatID, file), nil
|
animation := tgbotapi.NewAnimation(chatID, file)
|
||||||
|
animation.ChannelUsername = channelUsername
|
||||||
|
return animation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveTelegramParseMode(format channel.MessageFormat) string {
|
func resolveTelegramParseMode(format channel.MessageFormat) string {
|
||||||
@@ -1424,8 +1416,8 @@ func (a *TelegramAdapter) ResolveAttachment(ctx context.Context, cfg channel.Cha
|
|||||||
mime := strings.TrimSpace(attachment.Mime)
|
mime := strings.TrimSpace(attachment.Mime)
|
||||||
if mime == "" {
|
if mime == "" {
|
||||||
mime = strings.TrimSpace(resp.Header.Get("Content-Type"))
|
mime = strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||||
if idx := strings.Index(mime, ";"); idx >= 0 {
|
if base, _, ok := strings.Cut(mime, ";"); ok {
|
||||||
mime = strings.TrimSpace(mime[:idx])
|
mime = strings.TrimSpace(base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size := attachment.Size
|
size := attachment.Size
|
||||||
|
|||||||
Reference in New Issue
Block a user