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)
|
||||
}
|
||||
|
||||
// apiEndpoint returns the Sprintf-formatted API endpoint derived from the base URL.
|
||||
func (c Config) apiEndpoint() string {
|
||||
// baseURL returns the effective base URL with trailing slashes removed.
|
||||
func (c Config) baseURL() string {
|
||||
base := c.APIBaseURL
|
||||
if base == "" {
|
||||
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.
|
||||
func (c Config) fileEndpoint() string {
|
||||
base := c.APIBaseURL
|
||||
if base == "" {
|
||||
base = defaultAPIBaseURL
|
||||
}
|
||||
return strings.TrimRight(base, "/") + "/file/bot%s/%s"
|
||||
return c.baseURL() + "/file/bot%s/%s"
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
return 0, 0
|
||||
}
|
||||
if idx := strings.Index(input, ":"); idx >= 0 {
|
||||
left := strings.TrimSpace(input[:idx])
|
||||
right := strings.TrimSpace(input[idx+1:])
|
||||
cid, err1 := strconv.ParseInt(left, 10, 64)
|
||||
uid, err2 := strconv.ParseInt(right, 10, 64)
|
||||
if left, right, ok := strings.Cut(input, ":"); ok {
|
||||
cid, err1 := strconv.ParseInt(strings.TrimSpace(left), 10, 64)
|
||||
uid, err2 := strconv.ParseInt(strings.TrimSpace(right), 10, 64)
|
||||
if err1 == nil && err2 == nil && cid != 0 && uid != 0 {
|
||||
return cid, uid
|
||||
}
|
||||
|
||||
@@ -265,6 +265,188 @@ func (s *telegramOutboundStream) sendPermanentMessage(ctx context.Context, text
|
||||
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 {
|
||||
if s == nil || s.adapter == nil {
|
||||
return errors.New("telegram stream not configured")
|
||||
@@ -278,196 +460,21 @@ func (s *telegramOutboundStream) Push(ctx context.Context, event channel.StreamE
|
||||
default:
|
||||
}
|
||||
switch event.Type {
|
||||
case channel.StreamEventStatus:
|
||||
return nil
|
||||
case channel.StreamEventToolCallStart:
|
||||
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.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
|
||||
return s.pushToolCallStart(ctx)
|
||||
case channel.StreamEventToolCallEnd:
|
||||
s.mu.Lock()
|
||||
s.streamMsgID = 0
|
||||
if !s.isPrivateChat {
|
||||
s.streamChatID = 0
|
||||
}
|
||||
s.lastEdited = ""
|
||||
s.lastEditedAt = time.Time{}
|
||||
s.buf.Reset()
|
||||
s.mu.Unlock()
|
||||
s.resetStreamState()
|
||||
return nil
|
||||
case channel.StreamEventAttachment:
|
||||
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
|
||||
case channel.StreamEventPhaseStart:
|
||||
return nil
|
||||
return s.pushAttachment(ctx, event)
|
||||
case channel.StreamEventPhaseEnd:
|
||||
if event.Phase == channel.StreamPhaseText {
|
||||
// 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
|
||||
return s.pushPhaseEnd(ctx, event)
|
||||
case channel.StreamEventDelta:
|
||||
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)
|
||||
return s.pushDelta(ctx, event)
|
||||
case channel.StreamEventFinal:
|
||||
// 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 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
|
||||
return s.pushFinal(ctx, event)
|
||||
case channel.StreamEventError:
|
||||
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)
|
||||
return s.pushError(ctx, event)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -434,8 +435,8 @@ func (a *TelegramAdapter) buildTelegramMediaGroupInboundMessage(
|
||||
if len(items) == 0 {
|
||||
return channel.InboundMessage{}, false
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].MessageID < items[j].MessageID
|
||||
slices.SortStableFunc(items, func(a, b *tgbotapi.Message) int {
|
||||
return cmp.Compare(a.MessageID, b.MessageID)
|
||||
})
|
||||
anchor := items[0]
|
||||
text := ""
|
||||
@@ -743,32 +744,25 @@ func sendTelegramTextReturnMessage(bot *tgbotapi.BotAPI, target string, text str
|
||||
if sendTextForTest != nil {
|
||||
return sendTextForTest(bot, target, text, replyTo, parseMode)
|
||||
}
|
||||
var sent tgbotapi.Message
|
||||
if strings.HasPrefix(target, "@") {
|
||||
message := tgbotapi.NewMessageToChannel(target, text)
|
||||
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
|
||||
}
|
||||
parsedChatID, channelUsername, parseErr := parseTelegramTarget(target)
|
||||
if parseErr != nil {
|
||||
return 0, 0, parseErr
|
||||
}
|
||||
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 {
|
||||
chatID = sent.Chat.ID
|
||||
}
|
||||
@@ -875,17 +869,17 @@ func sendTelegramAttachmentImpl(ctx context.Context, bot *tgbotapi.BotAPI, targe
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isChannel := strings.HasPrefix(target, "@")
|
||||
chatID, channelUsername, targetErr := parseTelegramTarget(target)
|
||||
if targetErr != nil {
|
||||
return targetErr
|
||||
}
|
||||
isChannel := channelUsername != ""
|
||||
switch att.Type {
|
||||
case channel.AttachmentImage:
|
||||
var photo tgbotapi.PhotoConfig
|
||||
if isChannel {
|
||||
photo = tgbotapi.NewPhotoToChannel(target, file)
|
||||
photo = tgbotapi.NewPhotoToChannel(channelUsername, file)
|
||||
} 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.Caption = caption
|
||||
@@ -900,15 +894,11 @@ func sendTelegramAttachmentImpl(ctx context.Context, bot *tgbotapi.BotAPI, targe
|
||||
if isChannel {
|
||||
document = tgbotapi.DocumentConfig{
|
||||
BaseFile: tgbotapi.BaseFile{
|
||||
BaseChat: tgbotapi.BaseChat{ChannelUsername: target},
|
||||
BaseChat: tgbotapi.BaseChat{ChannelUsername: channelUsername},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
} 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.Caption = caption
|
||||
@@ -1017,8 +1007,8 @@ func resolveTelegramFile(ctx context.Context, urlRef, keyRef, base64Ref, sourceP
|
||||
|
||||
func decodeDataURLBytes(dataURL string) ([]byte, error) {
|
||||
value := dataURL
|
||||
if idx := strings.Index(value, ","); idx >= 0 {
|
||||
value = value[idx+1:]
|
||||
if _, after, ok := strings.Cut(value, ","); ok {
|
||||
value = after
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(
|
||||
base64StdDecoder(strings.NewReader(value)),
|
||||
@@ -1155,56 +1145,58 @@ func buildTelegramForwardContext(msg *tgbotapi.Message) string {
|
||||
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, "@") {
|
||||
audio := tgbotapi.NewAudio(0, file)
|
||||
audio.ChannelUsername = target
|
||||
return audio, nil
|
||||
return 0, target, nil
|
||||
}
|
||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
||||
chatID, err = strconv.ParseInt(target, 10, 64)
|
||||
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) {
|
||||
if strings.HasPrefix(target, "@") {
|
||||
voice := tgbotapi.NewVoice(0, file)
|
||||
voice.ChannelUsername = target
|
||||
return voice, nil
|
||||
}
|
||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
||||
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||
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) {
|
||||
if strings.HasPrefix(target, "@") {
|
||||
video := tgbotapi.NewVideo(0, file)
|
||||
video.ChannelUsername = target
|
||||
return video, nil
|
||||
}
|
||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
||||
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||
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) {
|
||||
if strings.HasPrefix(target, "@") {
|
||||
animation := tgbotapi.NewAnimation(0, file)
|
||||
animation.ChannelUsername = target
|
||||
return animation, nil
|
||||
}
|
||||
chatID, err := strconv.ParseInt(target, 10, 64)
|
||||
chatID, channelUsername, err := parseTelegramTarget(target)
|
||||
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 {
|
||||
@@ -1424,8 +1416,8 @@ func (a *TelegramAdapter) ResolveAttachment(ctx context.Context, cfg channel.Cha
|
||||
mime := strings.TrimSpace(attachment.Mime)
|
||||
if mime == "" {
|
||||
mime = strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||
if idx := strings.Index(mime, ";"); idx >= 0 {
|
||||
mime = strings.TrimSpace(mime[:idx])
|
||||
if base, _, ok := strings.Cut(mime, ";"); ok {
|
||||
mime = strings.TrimSpace(base)
|
||||
}
|
||||
}
|
||||
size := attachment.Size
|
||||
|
||||
Reference in New Issue
Block a user