feat(telegram): add guest mode support
- Add guest_mode configuration option for Telegram channel - Implement guest query handling when guest_mode is enabled - Add unit tests for guest mode functionality - Update UI form with guest mode toggle - Add translations for guest mode strings
This commit is contained in:
parent
84164e47f3
commit
361a53a2b3
9 changed files with 520 additions and 20 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -30,6 +32,8 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const telegramGuestMessageUpdates = "guest_message"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`)
|
reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`)
|
||||||
reBlockquote = regexp.MustCompile(`^>\s*(.*)$`)
|
reBlockquote = regexp.MustCompile(`^>\s*(.*)$`)
|
||||||
|
|
@ -43,7 +47,7 @@ var (
|
||||||
reInlineCode = regexp.MustCompile("`([^`]+)`")
|
reInlineCode = regexp.MustCompile("`([^`]+)`")
|
||||||
)
|
)
|
||||||
|
|
||||||
func telegramAllowedUpdates(businessMode bool) []string {
|
func telegramAllowedUpdates(businessMode, guestMode bool) []string {
|
||||||
updates := []string{
|
updates := []string{
|
||||||
telego.MessageUpdates,
|
telego.MessageUpdates,
|
||||||
telego.EditedMessageUpdates,
|
telego.EditedMessageUpdates,
|
||||||
|
|
@ -65,6 +69,9 @@ func telegramAllowedUpdates(businessMode bool) []string {
|
||||||
telego.DeletedBusinessMessagesUpdates,
|
telego.DeletedBusinessMessagesUpdates,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if guestMode {
|
||||||
|
updates = append(updates, telegramGuestMessageUpdates)
|
||||||
|
}
|
||||||
return updates
|
return updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +89,7 @@ type TelegramChannel struct {
|
||||||
registerFunc func(context.Context, []commands.Definition) error
|
registerFunc func(context.Context, []commands.Definition) error
|
||||||
commandRegDelayFn func(int) time.Duration
|
commandRegDelayFn func(int) time.Duration
|
||||||
commandRegCancel context.CancelFunc
|
commandRegCancel context.CancelFunc
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTelegramChannel(
|
func NewTelegramChannel(
|
||||||
|
|
@ -137,6 +145,7 @@ func NewTelegramChannel(
|
||||||
bc: bc,
|
bc: bc,
|
||||||
chatIDs: make(map[string]int64),
|
chatIDs: make(map[string]int64),
|
||||||
tgCfg: telegramCfg,
|
tgCfg: telegramCfg,
|
||||||
|
httpClient: newTelegramHTTPClient(telegramCfg),
|
||||||
}
|
}
|
||||||
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
||||||
return ch, nil
|
return ch, nil
|
||||||
|
|
@ -147,13 +156,20 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
updates, err := c.bot.UpdatesViaLongPolling(c.ctx, &telego.GetUpdatesParams{
|
updateParams := &telego.GetUpdatesParams{
|
||||||
Timeout: 30,
|
Timeout: 30,
|
||||||
AllowedUpdates: telegramAllowedUpdates(c.businessModeEnabled()),
|
AllowedUpdates: telegramAllowedUpdates(c.businessModeEnabled(), c.guestModeEnabled()),
|
||||||
})
|
}
|
||||||
if err != nil {
|
var updates <-chan telego.Update
|
||||||
c.cancel()
|
if c.guestModeEnabled() {
|
||||||
return fmt.Errorf("failed to start long polling: %w", err)
|
updates = c.updatesViaLongPollingWithGuest(c.ctx, updateParams)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
updates, err = c.bot.UpdatesViaLongPolling(c.ctx, updateParams)
|
||||||
|
if err != nil {
|
||||||
|
c.cancel()
|
||||||
|
return fmt.Errorf("failed to start long polling: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bh, err := th.NewBotHandler(c.bot, updates)
|
bh, err := th.NewBotHandler(c.bot, updates)
|
||||||
|
|
@ -222,6 +238,138 @@ func (c *TelegramChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTelegramHTTPClient(telegramCfg *config.TelegramSettings) *http.Client {
|
||||||
|
if telegramCfg == nil || strings.TrimSpace(telegramCfg.Proxy) == "" {
|
||||||
|
return http.DefaultClient
|
||||||
|
}
|
||||||
|
proxyURL, err := url.Parse(telegramCfg.Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return http.DefaultClient
|
||||||
|
}
|
||||||
|
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) telegramAPIBaseURL() string {
|
||||||
|
if c != nil && c.tgCfg != nil {
|
||||||
|
if baseURL := strings.TrimRight(strings.TrimSpace(c.tgCfg.BaseURL), "/"); baseURL != "" {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "https://api.telegram.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) postTelegramAPI(ctx context.Context, method string, params any, result any) error {
|
||||||
|
if c == nil || c.bot == nil {
|
||||||
|
return fmt.Errorf("telegram bot is nil")
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
endpoint := fmt.Sprintf("%s/bot%s/%s", c.telegramAPIBaseURL(), c.bot.Token(), method)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := c.httpClient
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var apiResp struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Result json.RawMessage `json:"result"`
|
||||||
|
ErrorCode int `json:"error_code,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !apiResp.OK {
|
||||||
|
if apiResp.Description != "" {
|
||||||
|
return fmt.Errorf("telegram %s: %s", method, apiResp.Description)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("telegram %s: HTTP %d", method, resp.StatusCode)
|
||||||
|
}
|
||||||
|
if result != nil && len(apiResp.Result) > 0 {
|
||||||
|
if err := json.Unmarshal(apiResp.Result, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type telegramRawUpdate struct {
|
||||||
|
telego.Update
|
||||||
|
GuestMessage *telegramGuestMessage `json:"guest_message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) updatesViaLongPollingWithGuest(
|
||||||
|
ctx context.Context,
|
||||||
|
params *telego.GetUpdatesParams,
|
||||||
|
) <-chan telego.Update {
|
||||||
|
updates := make(chan telego.Update, 100)
|
||||||
|
go c.pollTelegramUpdatesWithGuest(ctx, params, updates)
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) pollTelegramUpdatesWithGuest(
|
||||||
|
ctx context.Context,
|
||||||
|
params *telego.GetUpdatesParams,
|
||||||
|
updates chan<- telego.Update,
|
||||||
|
) {
|
||||||
|
defer close(updates)
|
||||||
|
if params == nil {
|
||||||
|
params = &telego.GetUpdatesParams{Timeout: 30}
|
||||||
|
}
|
||||||
|
pollParams := *params
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawUpdates []telegramRawUpdate
|
||||||
|
if err := c.postTelegramAPI(ctx, "getUpdates", &pollParams, &rawUpdates); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.ErrorCF("telegram", "Guest-mode long polling failed", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, update := range rawUpdates {
|
||||||
|
if update.UpdateID >= pollParams.Offset {
|
||||||
|
pollParams.Offset = update.UpdateID + 1
|
||||||
|
}
|
||||||
|
if update.GuestMessage != nil {
|
||||||
|
if err := c.handleGuestMessage(ctx, update.GuestMessage); err != nil {
|
||||||
|
logger.ErrorCF("telegram", "Guest message handling failed", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case updates <- update.Update:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return nil, channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
|
|
@ -239,6 +387,12 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]
|
||||||
}
|
}
|
||||||
|
|
||||||
isToolFeedback := outboundMessageIsToolFeedback(msg)
|
isToolFeedback := outboundMessageIsToolFeedback(msg)
|
||||||
|
if target.guestQueryID != "" {
|
||||||
|
if isToolFeedback {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return c.answerGuestQuery(ctx, target.guestQueryID, msg.Content, useMarkdownV2)
|
||||||
|
}
|
||||||
toolFeedbackContent := msg.Content
|
toolFeedbackContent := msg.Content
|
||||||
if isToolFeedback {
|
if isToolFeedback {
|
||||||
toolFeedbackContent = fitToolFeedbackForTelegram(msg.Content, useMarkdownV2, 4096)
|
toolFeedbackContent = fitToolFeedbackForTelegram(msg.Content, useMarkdownV2, 4096)
|
||||||
|
|
@ -401,6 +555,55 @@ func (c *TelegramChannel) sendChunk(
|
||||||
return strconv.Itoa(pMsg.MessageID), nil
|
return strconv.Itoa(pMsg.MessageID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) answerGuestQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
guestQueryID string,
|
||||||
|
content string,
|
||||||
|
useMarkdownV2 bool,
|
||||||
|
) ([]string, error) {
|
||||||
|
guestQueryID = strings.TrimSpace(guestQueryID)
|
||||||
|
if guestQueryID == "" {
|
||||||
|
return nil, fmt.Errorf("missing guest query ID: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
chunks := channels.SplitMessage(content, 4000)
|
||||||
|
if len(chunks) > 0 {
|
||||||
|
content = chunks[0]
|
||||||
|
}
|
||||||
|
parsedContent := parseContent(content, useMarkdownV2)
|
||||||
|
parseMode := telego.ModeHTML
|
||||||
|
if useMarkdownV2 {
|
||||||
|
parseMode = telego.ModeMarkdownV2
|
||||||
|
}
|
||||||
|
params := map[string]any{
|
||||||
|
"guest_query_id": guestQueryID,
|
||||||
|
"result": map[string]any{
|
||||||
|
"type": "article",
|
||||||
|
"id": "picoclaw-reply",
|
||||||
|
"title": "PicoClaw",
|
||||||
|
"input_message_content": map[string]any{
|
||||||
|
"message_text": parsedContent,
|
||||||
|
"parse_mode": parseMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var result struct {
|
||||||
|
MessageID int `json:"message_id,omitempty"`
|
||||||
|
}
|
||||||
|
if err := c.postTelegramAPI(ctx, "answerGuestQuery", params, &result); err != nil {
|
||||||
|
logParseFailed(err, useMarkdownV2)
|
||||||
|
params["result"].(map[string]any)["input_message_content"] = map[string]any{
|
||||||
|
"message_text": content,
|
||||||
|
}
|
||||||
|
if retryErr := c.postTelegramAPI(ctx, "answerGuestQuery", params, &result); retryErr != nil {
|
||||||
|
return nil, fmt.Errorf("telegram answer guest query: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.MessageID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []string{strconv.Itoa(result.MessageID)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// maxTypingDuration limits how long the typing indicator can run.
|
// maxTypingDuration limits how long the typing indicator can run.
|
||||||
// Prevents endless typing when the LLM fails/hangs and preSend never invokes cancel.
|
// Prevents endless typing when the LLM fails/hangs and preSend never invokes cancel.
|
||||||
// Matches channels.Manager's typingStopTTL (5 min) so behavior is consistent.
|
// Matches channels.Manager's typingStopTTL (5 min) so behavior is consistent.
|
||||||
|
|
@ -417,6 +620,9 @@ func (c *TelegramChannel) StartTyping(ctx context.Context, chatID string) (func(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return func() {}, err
|
return func() {}, err
|
||||||
}
|
}
|
||||||
|
if target.guestQueryID != "" {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
action := tu.ChatAction(tu.ID(target.chatID), telego.ChatActionTyping)
|
action := tu.ChatAction(tu.ID(target.chatID), telego.ChatActionTyping)
|
||||||
action.MessageThreadID = target.threadID
|
action.MessageThreadID = target.threadID
|
||||||
|
|
@ -455,10 +661,16 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if target.guestQueryID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
mid, err := strconv.Atoi(messageID)
|
mid, err := strconv.Atoi(messageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if target.guestQueryID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
parsedContent := parseContent(content, useMarkdownV2)
|
parsedContent := parseContent(content, useMarkdownV2)
|
||||||
editMsg := tu.EditMessageText(tu.ID(target.chatID), mid, parsedContent)
|
editMsg := tu.EditMessageText(tu.ID(target.chatID), mid, parsedContent)
|
||||||
editMsg.BusinessConnectionID = target.businessConnectionID
|
editMsg.BusinessConnectionID = target.businessConnectionID
|
||||||
|
|
@ -628,6 +840,9 @@ func (c *TelegramChannel) SendPlaceholder(ctx context.Context, chatID string) (s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if target.guestQueryID != "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
phMsg := tu.Message(tu.ID(target.chatID), text)
|
phMsg := tu.Message(tu.ID(target.chatID), text)
|
||||||
phMsg.MessageThreadID = target.threadID
|
phMsg.MessageThreadID = target.threadID
|
||||||
|
|
@ -769,13 +984,17 @@ func (c *TelegramChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
||||||
return c.handleTelegramMessage(ctx, message, "")
|
return c.handleTelegramMessage(ctx, message, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) businessModeEnabled() bool {
|
func (c *TelegramChannel) businessModeEnabled() bool {
|
||||||
return c != nil && c.tgCfg != nil && c.tgCfg.BusinessMode
|
return c != nil && c.tgCfg != nil && c.tgCfg.BusinessMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) guestModeEnabled() bool {
|
||||||
|
return c != nil && c.tgCfg != nil && c.tgCfg.GuestMode
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleBusinessMessage(ctx context.Context, message *telego.Message) error {
|
func (c *TelegramChannel) handleBusinessMessage(ctx context.Context, message *telego.Message) error {
|
||||||
if !c.businessModeEnabled() {
|
if !c.businessModeEnabled() {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -802,7 +1021,42 @@ func (c *TelegramChannel) handleBusinessMessage(ctx context.Context, message *te
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.markBusinessMessageRead(ctx, businessConnectionID, message.Chat.ID, message.MessageID)
|
c.markBusinessMessageRead(ctx, businessConnectionID, message.Chat.ID, message.MessageID)
|
||||||
return c.handleTelegramMessage(ctx, message, businessConnectionID)
|
return c.handleTelegramMessage(ctx, message, businessConnectionID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
type telegramGuestMessage struct {
|
||||||
|
telego.Message
|
||||||
|
GuestQueryID string `json:"guest_query_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *telegramGuestMessage) UnmarshalJSON(data []byte) error {
|
||||||
|
var message telego.Message
|
||||||
|
if err := json.Unmarshal(data, &message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var guestFields struct {
|
||||||
|
GuestQueryID string `json:"guest_query_id,omitempty"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &guestFields); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Message = message
|
||||||
|
m.GuestQueryID = guestFields.GuestQueryID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) handleGuestMessage(ctx context.Context, message *telegramGuestMessage) error {
|
||||||
|
if !c.guestModeEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if message == nil {
|
||||||
|
return fmt.Errorf("guest message is nil")
|
||||||
|
}
|
||||||
|
guestQueryID := strings.TrimSpace(message.GuestQueryID)
|
||||||
|
if guestQueryID == "" {
|
||||||
|
return fmt.Errorf("guest message missing guest_query_id")
|
||||||
|
}
|
||||||
|
return c.handleTelegramMessage(ctx, &message.Message, "", guestQueryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) isDisabledBusinessCommand(message *telego.Message) bool {
|
func (c *TelegramChannel) isDisabledBusinessCommand(message *telego.Message) bool {
|
||||||
|
|
@ -882,7 +1136,12 @@ func (c *TelegramChannel) handleDeletedBusinessMessages(_ context.Context, delet
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleTelegramMessage(ctx context.Context, message *telego.Message, businessConnectionID string) error {
|
func (c *TelegramChannel) handleTelegramMessage(
|
||||||
|
ctx context.Context,
|
||||||
|
message *telego.Message,
|
||||||
|
businessConnectionID string,
|
||||||
|
guestQueryID string,
|
||||||
|
) error {
|
||||||
if message == nil {
|
if message == nil {
|
||||||
return fmt.Errorf("message is nil")
|
return fmt.Errorf("message is nil")
|
||||||
}
|
}
|
||||||
|
|
@ -915,7 +1174,7 @@ func (c *TelegramChannel) handleTelegramMessage(ctx context.Context, message *te
|
||||||
content := ""
|
content := ""
|
||||||
mediaPaths := []string{}
|
mediaPaths := []string{}
|
||||||
|
|
||||||
chatIDStr := formatTelegramDeliveryChatID(chatID, businessConnectionID, 0)
|
chatIDStr := formatTelegramDeliveryChatID(chatID, businessConnectionID, guestQueryID, 0)
|
||||||
messageIDStr := fmt.Sprintf("%d", message.MessageID)
|
messageIDStr := fmt.Sprintf("%d", message.MessageID)
|
||||||
scope := channels.BuildMediaScope("telegram", chatIDStr, messageIDStr)
|
scope := channels.BuildMediaScope("telegram", chatIDStr, messageIDStr)
|
||||||
|
|
||||||
|
|
@ -1034,10 +1293,10 @@ func (c *TelegramChannel) handleTelegramMessage(ctx context.Context, message *te
|
||||||
// route to the correct topic and each topic gets its own session.
|
// route to the correct topic and each topic gets its own session.
|
||||||
// Only forum groups (IsForum) are handled; regular group reply threads
|
// Only forum groups (IsForum) are handled; regular group reply threads
|
||||||
// must share one session per group.
|
// must share one session per group.
|
||||||
compositeChatID := formatTelegramDeliveryChatID(chatID, businessConnectionID, 0)
|
compositeChatID := formatTelegramDeliveryChatID(chatID, businessConnectionID, guestQueryID, 0)
|
||||||
threadID := message.MessageThreadID
|
threadID := message.MessageThreadID
|
||||||
if message.Chat.IsForum && threadID != 0 {
|
if message.Chat.IsForum && threadID != 0 {
|
||||||
compositeChatID = formatTelegramDeliveryChatID(chatID, businessConnectionID, threadID)
|
compositeChatID = formatTelegramDeliveryChatID(chatID, businessConnectionID, guestQueryID, threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("telegram", "Received message", map[string]any{
|
logger.DebugCF("telegram", "Received message", map[string]any{
|
||||||
|
|
@ -1062,10 +1321,13 @@ func (c *TelegramChannel) handleTelegramMessage(ctx context.Context, message *te
|
||||||
if businessConnectionID != "" {
|
if businessConnectionID != "" {
|
||||||
metadata["business_connection_id"] = businessConnectionID
|
metadata["business_connection_id"] = businessConnectionID
|
||||||
}
|
}
|
||||||
|
if guestQueryID != "" {
|
||||||
|
metadata["guest_query_id"] = guestQueryID
|
||||||
|
}
|
||||||
|
|
||||||
inboundCtx := bus.InboundContext{
|
inboundCtx := bus.InboundContext{
|
||||||
Channel: c.Name(),
|
Channel: c.Name(),
|
||||||
ChatID: formatTelegramDeliveryChatID(chatID, businessConnectionID, 0),
|
ChatID: formatTelegramDeliveryChatID(chatID, businessConnectionID, guestQueryID, 0),
|
||||||
ChatType: peerKind,
|
ChatType: peerKind,
|
||||||
SenderID: platformID,
|
SenderID: platformID,
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
|
|
@ -1075,6 +1337,9 @@ func (c *TelegramChannel) handleTelegramMessage(ctx context.Context, message *te
|
||||||
if businessConnectionID != "" {
|
if businessConnectionID != "" {
|
||||||
inboundCtx.Account = businessConnectionID
|
inboundCtx.Account = businessConnectionID
|
||||||
}
|
}
|
||||||
|
if guestQueryID != "" {
|
||||||
|
inboundCtx.Account = guestQueryID
|
||||||
|
}
|
||||||
if message.Chat.IsForum && threadID != 0 {
|
if message.Chat.IsForum && threadID != 0 {
|
||||||
inboundCtx.TopicID = fmt.Sprintf("%d", threadID)
|
inboundCtx.TopicID = fmt.Sprintf("%d", threadID)
|
||||||
}
|
}
|
||||||
|
|
@ -1331,17 +1596,20 @@ func parseTelegramChatID(chatID string) (int64, int, error) {
|
||||||
|
|
||||||
type telegramOutboundTarget struct {
|
type telegramOutboundTarget struct {
|
||||||
businessConnectionID string
|
businessConnectionID string
|
||||||
|
guestQueryID string
|
||||||
chatID int64
|
chatID int64
|
||||||
threadID int
|
threadID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t telegramOutboundTarget) String() string {
|
func (t telegramOutboundTarget) String() string {
|
||||||
return formatTelegramDeliveryChatID(t.chatID, t.businessConnectionID, t.threadID)
|
return formatTelegramDeliveryChatID(t.chatID, t.businessConnectionID, t.guestQueryID, t.threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTelegramDeliveryChatID(chatID int64, businessConnectionID string, threadID int) string {
|
func formatTelegramDeliveryChatID(chatID int64, businessConnectionID string, guestQueryID string, threadID int) string {
|
||||||
base := fmt.Sprintf("%d", chatID)
|
base := fmt.Sprintf("%d", chatID)
|
||||||
if strings.TrimSpace(businessConnectionID) != "" {
|
if strings.TrimSpace(guestQueryID) != "" {
|
||||||
|
base = fmt.Sprintf("guest:%s:%d", url.QueryEscape(strings.TrimSpace(guestQueryID)), chatID)
|
||||||
|
} else if strings.TrimSpace(businessConnectionID) != "" {
|
||||||
base = fmt.Sprintf("business:%s:%d", url.QueryEscape(strings.TrimSpace(businessConnectionID)), chatID)
|
base = fmt.Sprintf("business:%s:%d", url.QueryEscape(strings.TrimSpace(businessConnectionID)), chatID)
|
||||||
}
|
}
|
||||||
if threadID != 0 {
|
if threadID != 0 {
|
||||||
|
|
@ -1372,6 +1640,26 @@ func parseTelegramOutboundTarget(chatID string) (telegramOutboundTarget, error)
|
||||||
threadID: threadID,
|
threadID: threadID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(targetChatID, "guest:") {
|
||||||
|
rest := strings.TrimPrefix(targetChatID, "guest:")
|
||||||
|
parts := strings.SplitN(rest, ":", 2)
|
||||||
|
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
|
||||||
|
return telegramOutboundTarget{}, fmt.Errorf("invalid guest chat ID %q", chatID)
|
||||||
|
}
|
||||||
|
guestQueryID, unescapeErr := url.QueryUnescape(strings.TrimSpace(parts[0]))
|
||||||
|
if unescapeErr != nil {
|
||||||
|
return telegramOutboundTarget{}, fmt.Errorf("invalid guest query ID in chat ID %q: %w", chatID, unescapeErr)
|
||||||
|
}
|
||||||
|
cid, threadID, err := parseTelegramChatID(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return telegramOutboundTarget{}, err
|
||||||
|
}
|
||||||
|
return telegramOutboundTarget{
|
||||||
|
guestQueryID: guestQueryID,
|
||||||
|
chatID: cid,
|
||||||
|
threadID: threadID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
cid, threadID, err := parseTelegramChatID(targetChatID)
|
cid, threadID, err := parseTelegramChatID(targetChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1380,6 +1668,18 @@ func parseTelegramOutboundTarget(chatID string) (telegramOutboundTarget, error)
|
||||||
return telegramOutboundTarget{chatID: cid, threadID: threadID}, nil
|
return telegramOutboundTarget{chatID: cid, threadID: threadID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func guestQueryIDFromContext(outboundCtx *bus.InboundContext) string {
|
||||||
|
if outboundCtx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if outboundCtx.Raw != nil {
|
||||||
|
if guestQueryID := strings.TrimSpace(outboundCtx.Raw["guest_query_id"]); guestQueryID != "" {
|
||||||
|
return guestQueryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func businessConnectionIDFromContext(outboundCtx *bus.InboundContext) string {
|
func businessConnectionIDFromContext(outboundCtx *bus.InboundContext) string {
|
||||||
if outboundCtx == nil {
|
if outboundCtx == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -1404,6 +1704,9 @@ func resolveTelegramOutboundTarget(chatID string, outboundCtx *bus.InboundContex
|
||||||
if resolved.businessConnectionID == "" {
|
if resolved.businessConnectionID == "" {
|
||||||
resolved.businessConnectionID = businessConnectionIDFromContext(outboundCtx)
|
resolved.businessConnectionID = businessConnectionIDFromContext(outboundCtx)
|
||||||
}
|
}
|
||||||
|
if resolved.guestQueryID == "" {
|
||||||
|
resolved.guestQueryID = guestQueryIDFromContext(outboundCtx)
|
||||||
|
}
|
||||||
if resolved.threadID != 0 || outboundCtx == nil {
|
if resolved.threadID != 0 || outboundCtx == nil {
|
||||||
return resolved, nil
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -208,12 +209,12 @@ func TestHandleBusinessMessage_EnabledBusinessCommandsForwardsCommand(t *testing
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTelegramAllowedUpdates_BusinessMode(t *testing.T) {
|
func TestTelegramAllowedUpdates_BusinessMode(t *testing.T) {
|
||||||
disabled := strings.Join(telegramAllowedUpdates(false), ",")
|
disabled := strings.Join(telegramAllowedUpdates(false, false), ",")
|
||||||
if strings.Contains(disabled, telego.BusinessMessageUpdates) {
|
if strings.Contains(disabled, telego.BusinessMessageUpdates) {
|
||||||
t.Fatalf("disabled updates include business messages: %s", disabled)
|
t.Fatalf("disabled updates include business messages: %s", disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled := strings.Join(telegramAllowedUpdates(true), ",")
|
enabled := strings.Join(telegramAllowedUpdates(true, false), ",")
|
||||||
if !strings.Contains(enabled, telego.BusinessMessageUpdates) {
|
if !strings.Contains(enabled, telego.BusinessMessageUpdates) {
|
||||||
t.Fatalf("enabled updates do not include business messages: %s", enabled)
|
t.Fatalf("enabled updates do not include business messages: %s", enabled)
|
||||||
}
|
}
|
||||||
|
|
@ -222,6 +223,129 @@ func TestTelegramAllowedUpdates_BusinessMode(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTelegramAllowedUpdates_GuestMode(t *testing.T) {
|
||||||
|
disabled := strings.Join(telegramAllowedUpdates(false, false), ",")
|
||||||
|
if strings.Contains(disabled, telegramGuestMessageUpdates) {
|
||||||
|
t.Fatalf("disabled updates include guest messages: %s", disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled := strings.Join(telegramAllowedUpdates(false, true), ",")
|
||||||
|
if !strings.Contains(enabled, telegramGuestMessageUpdates) {
|
||||||
|
t.Fatalf("enabled updates do not include guest messages: %s", enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleGuestMessage_DisabledGuestModeIgnoresMessage(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &TelegramChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||||
|
chatIDs: make(map[string]int64),
|
||||||
|
ctx: context.Background(),
|
||||||
|
tgCfg: &config.TelegramSettings{GuestMode: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &telegramGuestMessage{
|
||||||
|
Message: telego.Message{
|
||||||
|
Text: "ignored guest message",
|
||||||
|
MessageID: 18,
|
||||||
|
Chat: telego.Chat{
|
||||||
|
ID: 777,
|
||||||
|
Type: "private",
|
||||||
|
},
|
||||||
|
From: &telego.User{
|
||||||
|
ID: 42,
|
||||||
|
FirstName: "Alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GuestQueryID: "guest-query-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ch.handleGuestMessage(context.Background(), msg); err != nil {
|
||||||
|
t.Fatalf("handleGuestMessage error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case inbound := <-messageBus.InboundChan():
|
||||||
|
t.Fatalf("expected disabled guest mode to ignore message, got %#v", inbound)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleGuestMessage_ForwardsWithGuestContext(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &TelegramChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||||
|
chatIDs: make(map[string]int64),
|
||||||
|
ctx: context.Background(),
|
||||||
|
tgCfg: &config.TelegramSettings{GuestMode: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &telegramGuestMessage{
|
||||||
|
Message: telego.Message{
|
||||||
|
Text: "hello from guest",
|
||||||
|
MessageID: 17,
|
||||||
|
Chat: telego.Chat{
|
||||||
|
ID: 777,
|
||||||
|
Type: "private",
|
||||||
|
},
|
||||||
|
From: &telego.User{
|
||||||
|
ID: 42,
|
||||||
|
FirstName: "Alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GuestQueryID: "guest-query-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ch.handleGuestMessage(context.Background(), msg); err != nil {
|
||||||
|
t.Fatalf("handleGuestMessage error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound, ok := <-messageBus.InboundChan()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected inbound message to be forwarded")
|
||||||
|
}
|
||||||
|
if inbound.ChatID != "guest:guest-query-1:777" {
|
||||||
|
t.Fatalf("chat_id=%q", inbound.ChatID)
|
||||||
|
}
|
||||||
|
if inbound.Context.Account != "guest-query-1" {
|
||||||
|
t.Fatalf("account=%q", inbound.Context.Account)
|
||||||
|
}
|
||||||
|
if inbound.Context.Raw["guest_query_id"] != "guest-query-1" {
|
||||||
|
t.Fatalf("guest_query_id=%q", inbound.Context.Raw["guest_query_id"])
|
||||||
|
}
|
||||||
|
if inbound.Content != "hello from guest" {
|
||||||
|
t.Fatalf("content=%q", inbound.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTelegramRawUpdateDecodesGuestQueryID(t *testing.T) {
|
||||||
|
payload := []byte(`{
|
||||||
|
"update_id": 1001,
|
||||||
|
"guest_message": {
|
||||||
|
"message_id": 17,
|
||||||
|
"guest_query_id": "guest-query-1",
|
||||||
|
"date": 1760000000,
|
||||||
|
"chat": {"id": 777, "type": "private"},
|
||||||
|
"from": {"id": 42, "is_bot": false, "first_name": "Alice"},
|
||||||
|
"text": "hello from guest"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var update telegramRawUpdate
|
||||||
|
if err := json.Unmarshal(payload, &update); err != nil {
|
||||||
|
t.Fatalf("unmarshal raw update: %v", err)
|
||||||
|
}
|
||||||
|
if update.GuestMessage == nil {
|
||||||
|
t.Fatal("guest message was not decoded")
|
||||||
|
}
|
||||||
|
if update.GuestMessage.GuestQueryID != "guest-query-1" {
|
||||||
|
t.Fatalf("guest_query_id=%q", update.GuestMessage.GuestQueryID)
|
||||||
|
}
|
||||||
|
if update.GuestMessage.Text != "hello from guest" {
|
||||||
|
t.Fatalf("text=%q", update.GuestMessage.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleBusinessMessage_ForwardsWithBusinessContext(t *testing.T) {
|
func TestHandleBusinessMessage_ForwardsWithBusinessContext(t *testing.T) {
|
||||||
messageBus := bus.NewMessageBus()
|
messageBus := bus.NewMessageBus()
|
||||||
ch := &TelegramChannel{
|
ch := &TelegramChannel{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -390,6 +392,59 @@ func TestSend_BusinessMessageIncludesBusinessConnectionID(t *testing.T) {
|
||||||
assert.Equal(t, "hello business", params.Text)
|
assert.Equal(t, "hello business", params.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSend_GuestMessageUsesAnswerGuestQuery(t *testing.T) {
|
||||||
|
var requestPath string
|
||||||
|
var params struct {
|
||||||
|
GuestQueryID string `json:"guest_query_id"`
|
||||||
|
Result struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
InputMessageContent struct {
|
||||||
|
MessageText string `json:"message_text"`
|
||||||
|
ParseMode string `json:"parse_mode"`
|
||||||
|
} `json:"input_message_content"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestPath = r.URL.Path
|
||||||
|
require.NoError(t, json.NewDecoder(r.Body).Decode(¶ms))
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":55}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ch := newTestChannel(t, &stubCaller{
|
||||||
|
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
|
||||||
|
t.Fatalf("guest reply must not use telego caller: %s", url)
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ch.tgCfg.BaseURL = server.URL
|
||||||
|
|
||||||
|
ids, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
|
ChatID: "guest:guest-query-1:777",
|
||||||
|
Content: "hello guest",
|
||||||
|
Context: bus.InboundContext{
|
||||||
|
Channel: "telegram",
|
||||||
|
ChatID: "guest:guest-query-1:777",
|
||||||
|
Account: "guest-query-1",
|
||||||
|
Raw: map[string]string{
|
||||||
|
"guest_query_id": "guest-query-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"55"}, ids)
|
||||||
|
assert.Equal(t, "/bot"+testToken+"/answerGuestQuery", requestPath)
|
||||||
|
assert.Equal(t, "guest-query-1", params.GuestQueryID)
|
||||||
|
assert.Equal(t, "article", params.Result.Type)
|
||||||
|
assert.Equal(t, "picoclaw-reply", params.Result.ID)
|
||||||
|
assert.Equal(t, "PicoClaw", params.Result.Title)
|
||||||
|
assert.Equal(t, "hello guest", params.Result.InputMessageContent.MessageText)
|
||||||
|
assert.Equal(t, telego.ModeHTML, params.Result.InputMessageContent.ParseMode)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSend_NonToolFeedbackDeletesTrackedProgressMessage(t *testing.T) {
|
func TestSend_NonToolFeedbackDeletesTrackedProgressMessage(t *testing.T) {
|
||||||
caller := &stubCaller{
|
caller := &stubCaller{
|
||||||
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
|
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
|
||||||
|
|
@ -806,7 +861,7 @@ func TestParseTelegramChatID_InvalidThreadID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTelegramOutboundTarget_BusinessConnectionIDWithColon(t *testing.T) {
|
func TestParseTelegramOutboundTarget_BusinessConnectionIDWithColon(t *testing.T) {
|
||||||
formatted := formatTelegramDeliveryChatID(777, "biz:conn:1", 42)
|
formatted := formatTelegramDeliveryChatID(777, "biz:conn:1", "", 42)
|
||||||
|
|
||||||
target, err := parseTelegramOutboundTarget(formatted)
|
target, err := parseTelegramOutboundTarget(formatted)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,7 @@ type TelegramSettings struct {
|
||||||
Proxy string `json:"proxy" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
Proxy string `json:"proxy" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||||
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
Streaming StreamingConfig `json:"streaming,omitempty" yaml:"-"`
|
||||||
BusinessMode bool `json:"business_mode" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_MODE"`
|
BusinessMode bool `json:"business_mode" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_MODE"`
|
||||||
|
GuestMode bool `json:"guest_mode" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_GUEST_MODE"`
|
||||||
BusinessOwner string `json:"business_owner" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_OWNER"`
|
BusinessOwner string `json:"business_owner" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_OWNER"`
|
||||||
BusinessCommandsEnable bool `json:"business_commands_enable" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_COMMANDS_ENABLE"`
|
BusinessCommandsEnable bool `json:"business_commands_enable" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_BUSINESS_COMMANDS_ENABLE"`
|
||||||
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
|
UseMarkdownV2 bool `json:"use_markdown_v2" yaml:"-" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,7 @@ func defaultChannels() ChannelsConfig {
|
||||||
"settings": map[string]any{
|
"settings": map[string]any{
|
||||||
"streaming": map[string]any{"enabled": true, "throttle_seconds": 3, "min_growth_chars": 200},
|
"streaming": map[string]any{"enabled": true, "throttle_seconds": 3, "min_growth_chars": 200},
|
||||||
"business_mode": false,
|
"business_mode": false,
|
||||||
|
"guest_mode": false,
|
||||||
"business_owner": "",
|
"business_owner": "",
|
||||||
"business_commands_enable": false,
|
"business_commands_enable": false,
|
||||||
"use_markdown_v2": false,
|
"use_markdown_v2": false,
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,16 @@ export function TelegramForm({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SwitchCardField
|
||||||
|
label={t("channels.field.guestMode")}
|
||||||
|
hint={t("channels.form.desc.guestMode")}
|
||||||
|
checked={asBool(config.guest_mode)}
|
||||||
|
onCheckedChange={(checked) => onChange("guest_mode", checked)}
|
||||||
|
ariaLabel={t("channels.field.guestMode")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={t("channels.field.businessOwner")}
|
label={t("channels.field.businessOwner")}
|
||||||
hint={t("channels.form.desc.businessOwner")}
|
hint={t("channels.form.desc.businessOwner")}
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@
|
||||||
"encryptKey": "Encrypt Key",
|
"encryptKey": "Encrypt Key",
|
||||||
"baseUrl": "API Base URL",
|
"baseUrl": "API Base URL",
|
||||||
"businessMode": "Business Mode",
|
"businessMode": "Business Mode",
|
||||||
|
"guestMode": "Guest Mode",
|
||||||
"businessOwner": "Business Owner",
|
"businessOwner": "Business Owner",
|
||||||
"businessCommandsEnable": "Business Commands",
|
"businessCommandsEnable": "Business Commands",
|
||||||
"proxy": "HTTP Proxy",
|
"proxy": "HTTP Proxy",
|
||||||
|
|
@ -418,6 +419,7 @@
|
||||||
"encryptKey": "Encryption key used to decrypt callback payloads.",
|
"encryptKey": "Encryption key used to decrypt callback payloads.",
|
||||||
"baseUrl": "Platform API base URL. Official endpoint is used by default.",
|
"baseUrl": "Platform API base URL. Official endpoint is used by default.",
|
||||||
"businessMode": "Receive and reply to Telegram Business messages for connected business accounts.",
|
"businessMode": "Receive and reply to Telegram Business messages for connected business accounts.",
|
||||||
|
"guestMode": "Receive and reply to Telegram guest messages from chats where the bot is not a member.",
|
||||||
"businessOwner": "Telegram user ID of the business account owner. Business messages from this user are ignored.",
|
"businessOwner": "Telegram user ID of the business account owner. Business messages from this user are ignored.",
|
||||||
"businessCommandsEnable": "Allow bot commands such as /new in Telegram Business chats.",
|
"businessCommandsEnable": "Allow bot commands such as /new in Telegram Business chats.",
|
||||||
"proxy": "HTTP proxy address for outbound network access.",
|
"proxy": "HTTP proxy address for outbound network access.",
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,7 @@
|
||||||
"encryptKey": "Chave de Criptografia",
|
"encryptKey": "Chave de Criptografia",
|
||||||
"baseUrl": "URL Base da API",
|
"baseUrl": "URL Base da API",
|
||||||
"businessMode": "Modo Business",
|
"businessMode": "Modo Business",
|
||||||
|
"guestMode": "Modo Guest",
|
||||||
"businessOwner": "Proprietário Business",
|
"businessOwner": "Proprietário Business",
|
||||||
"businessCommandsEnable": "Comandos Business",
|
"businessCommandsEnable": "Comandos Business",
|
||||||
"proxy": "Proxy HTTP",
|
"proxy": "Proxy HTTP",
|
||||||
|
|
@ -410,6 +411,7 @@
|
||||||
"encryptKey": "Chave de criptografia usada para descriptografar payloads de callback.",
|
"encryptKey": "Chave de criptografia usada para descriptografar payloads de callback.",
|
||||||
"baseUrl": "URL base da API da plataforma. O endpoint oficial é usado por padrão.",
|
"baseUrl": "URL base da API da plataforma. O endpoint oficial é usado por padrão.",
|
||||||
"businessMode": "Receber e responder mensagens do Telegram Business para contas comerciais conectadas.",
|
"businessMode": "Receber e responder mensagens do Telegram Business para contas comerciais conectadas.",
|
||||||
|
"guestMode": "Receber e responder mensagens Guest do Telegram em chats onde o bot não é membro.",
|
||||||
"businessOwner": "ID de usuário do Telegram do proprietário da conta comercial. Mensagens Business desse usuário são ignoradas.",
|
"businessOwner": "ID de usuário do Telegram do proprietário da conta comercial. Mensagens Business desse usuário são ignoradas.",
|
||||||
"businessCommandsEnable": "Permitir comandos do bot, como /new, em chats do Telegram Business.",
|
"businessCommandsEnable": "Permitir comandos do bot, como /new, em chats do Telegram Business.",
|
||||||
"proxy": "Endereço de proxy HTTP para acesso de rede de saída.",
|
"proxy": "Endereço de proxy HTTP para acesso de rede de saída.",
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@
|
||||||
"encryptKey": "Encrypt Key",
|
"encryptKey": "Encrypt Key",
|
||||||
"baseUrl": "API Base URL",
|
"baseUrl": "API Base URL",
|
||||||
"businessMode": "Business Mode",
|
"businessMode": "Business Mode",
|
||||||
|
"guestMode": "Guest Mode",
|
||||||
"businessOwner": "Business Owner",
|
"businessOwner": "Business Owner",
|
||||||
"businessCommandsEnable": "Business Commands",
|
"businessCommandsEnable": "Business Commands",
|
||||||
"proxy": "HTTP 代理",
|
"proxy": "HTTP 代理",
|
||||||
|
|
@ -418,6 +419,7 @@
|
||||||
"encryptKey": "消息加密密钥,用于解密回调内容",
|
"encryptKey": "消息加密密钥,用于解密回调内容",
|
||||||
"baseUrl": "平台 API 地址,默认使用官方地址",
|
"baseUrl": "平台 API 地址,默认使用官方地址",
|
||||||
"businessMode": "接收并回复已连接商业账号的 Telegram Business 消息",
|
"businessMode": "接收并回复已连接商业账号的 Telegram Business 消息",
|
||||||
|
"guestMode": "接收并回复机器人未加入聊天中的 Telegram Guest 消息",
|
||||||
"businessOwner": "商业账号所有者的 Telegram 用户 ID。来自该用户的 Business 消息会被忽略。",
|
"businessOwner": "商业账号所有者的 Telegram 用户 ID。来自该用户的 Business 消息会被忽略。",
|
||||||
"businessCommandsEnable": "允许在 Telegram Business 聊天中处理 /new 等机器人命令。",
|
"businessCommandsEnable": "允许在 Telegram Business 聊天中处理 /new 等机器人命令。",
|
||||||
"proxy": "HTTP 代理地址,用于网络访问",
|
"proxy": "HTTP 代理地址,用于网络访问",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue