diff --git a/internal/service/organizer.go b/internal/service/organizer.go
index 8f79f71..73f6df4 100644
--- a/internal/service/organizer.go
+++ b/internal/service/organizer.go
@@ -31,10 +31,11 @@ import (
// OrganizerService moves/renames files into library structures.
type OrganizerService struct {
- cfg *config.Config
- log *zap.Logger
- repo *repository.Container
- probe *FFprobeService // optional; used for 洗版 resolution comparison
+ cfg *config.Config
+ log *zap.Logger
+ repo *repository.Container
+ probe *FFprobeService // optional; used for 洗版 resolution comparison
+ scraper *ScraperService // optional; used to identify metadata before rename
}
// NewOrganizerService is the constructor.
@@ -47,6 +48,10 @@ func NewOrganizerService(cfg *config.Config, log *zap.Logger, repo *repository.C
// Optional: when nil the organizer falls back to filename resolution tokens.
func (o *OrganizerService) SetProbe(p *FFprobeService) { o.probe = p }
+// SetScraper wires the scraper so directory organize can resolve TMDb/Bangumi
+// metadata before it decides the final folder and filename.
+func (o *OrganizerService) SetScraper(s *ScraperService) { o.scraper = s }
+
// OrganizeResult reports what happened.
type OrganizeResult struct {
Organized int `json:"organized"`
diff --git a/internal/service/organizer_directory.go b/internal/service/organizer_directory.go
index cae7703..0f4e0c7 100644
--- a/internal/service/organizer_directory.go
+++ b/internal/service/organizer_directory.go
@@ -138,6 +138,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
}
mode := o.resolveTransferMode(ctx, opts.TransferMode)
res := &OrganizeResult{SourcePath: source, DestPath: dest, DryRun: opts.DryRun}
+ metadataCache := map[string]*Match{}
if !info.IsDir() {
ext := strings.ToLower(filepath.Ext(source))
if _, ok := videoExtensions[ext]; !ok {
@@ -157,7 +158,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
)
return res, nil
}
- if err := o.organizeSourceFile(ctx, source, filepath.Dir(source), dest, mode, opts.MediaType, opts.MediaCategory, opts.DryRun, opts.AllowReplaceExisting, res); err != nil {
+ if err := o.organizeSourceFile(ctx, source, filepath.Dir(source), dest, mode, opts.MediaType, opts.MediaCategory, opts.DryRun, opts.AllowReplaceExisting, metadataCache, res); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", filepath.Base(source), err.Error()))
res.Items = append(res.Items, OrganizePreviewItem{Source: source, Action: "error", Reason: err.Error()})
}
@@ -185,7 +186,7 @@ func (o *OrganizerService) OrganizeDirectory(ctx context.Context, opts OrganizeO
res.Items = append(res.Items, OrganizePreviewItem{Source: path, Action: "skip", Reason: reason})
return nil
}
- if err := o.organizeSourceFile(ctx, path, source, dest, mode, opts.MediaType, opts.MediaCategory, opts.DryRun, opts.AllowReplaceExisting, res); err != nil {
+ if err := o.organizeSourceFile(ctx, path, source, dest, mode, opts.MediaType, opts.MediaCategory, opts.DryRun, opts.AllowReplaceExisting, metadataCache, res); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", filepath.Base(path), err.Error()))
res.Items = append(res.Items, OrganizePreviewItem{Source: path, Action: "error", Reason: err.Error()})
}
@@ -239,7 +240,7 @@ type organizeDirectoryLayout struct {
// organizeSourceFile organizes a single video file from the source directory
// into destRoot, applying dedup + 洗版.
-func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, sourceRoot, destRoot string, mode TransferMode, mediaTypeOverride, mediaCategoryOverride string, dryRun bool, allowReplaceExisting bool, res *OrganizeResult) error {
+func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, sourceRoot, destRoot string, mode TransferMode, mediaTypeOverride, mediaCategoryOverride string, dryRun bool, allowReplaceExisting bool, metadataCache map[string]*Match, res *OrganizeResult) error {
ext := filepath.Ext(src)
title, year := CleanQuery(src)
if title == "" {
@@ -263,6 +264,15 @@ func (o *OrganizerService) organizeSourceFile(ctx context.Context, src, sourceRo
if layout.MediaType == "" {
layout.MediaType = o.inferMediaTypeForSourceFile(src, title, season, episode)
}
+ if match := o.lookupOrganizeMetadata(ctx, src, sourceRoot, layout.MediaType, title, year, season, episode, metadataCache); match != nil {
+ if matchedTitle := sanitizeFilename(strings.TrimSpace(match.Title)); matchedTitle != "" {
+ title = matchedTitle
+ parsedTitle = strings.TrimSpace(match.Title)
+ }
+ if match.Year > 0 {
+ year = match.Year
+ }
+ }
if category := strings.TrimSpace(mediaCategoryOverride); category != "" {
layout.Category = sanitizeFilename(category)
} else if layout.Category == "" {
@@ -445,6 +455,91 @@ func (o *OrganizerService) inferMediaTypeForSourceFile(src, title string, season
return normalizeMediaType("", title, src)
}
+func (o *OrganizerService) lookupOrganizeMetadata(ctx context.Context, src, sourceRoot, mediaType, title string, year, season, episode int, cache map[string]*Match) *Match {
+ seriesLike := isSeriesLibraryType(mediaType) || season > 0 || episode > 0
+ if local, err := ReadLocalMetadata(src, sourceRoot, seriesLike); err == nil && local != nil {
+ if match := organizeMatchFromLocalMetadata(local); match != nil {
+ return match
+ }
+ } else if err != nil && o.log != nil {
+ o.log.Debug("organize read local metadata before rename failed", zap.String("path", src), zap.Error(err))
+ }
+ if o == nil || o.scraper == nil || !o.scraper.AnyEnabled() {
+ return nil
+ }
+ libType := normalizeOrganizeMediaType(mediaType)
+ if libType == "" {
+ libType = organizeLibraryModelType(mediaType)
+ }
+ lib := &model.Library{Path: sourceRoot, Type: libType, Enabled: true}
+ media := &model.Media{
+ Title: title,
+ Year: year,
+ Path: src,
+ SeasonNum: season,
+ EpisodeNum: episode,
+ }
+ for _, candidate := range scrapeQueryCandidates(media, lib) {
+ key := organizeMetadataCacheKey(lib.Type, candidate, year)
+ if cache != nil {
+ if cached, ok := cache[key]; ok {
+ if cached != nil {
+ return cached
+ }
+ continue
+ }
+ }
+ match := o.scraper.lookup(ctx, lib, candidate, year)
+ if cache != nil {
+ cache[key] = match
+ }
+ if match != nil && strings.TrimSpace(match.Title) != "" {
+ if o.log != nil {
+ o.log.Info("organize metadata matched before rename",
+ zap.String("source", src),
+ zap.String("query", candidate),
+ zap.String("title", match.Title),
+ zap.Int("year", match.Year),
+ zap.Int("tmdb_id", match.TMDbID),
+ zap.Int("bangumi_id", match.BangumiID))
+ }
+ return match
+ }
+ }
+ return nil
+}
+
+func organizeMatchFromLocalMetadata(local *LocalMetadata) *Match {
+ if local == nil || strings.TrimSpace(local.Title) == "" {
+ return nil
+ }
+ match := &Match{
+ Title: strings.TrimSpace(local.Title),
+ OriginalName: strings.TrimSpace(local.OriginalName),
+ Overview: local.Overview,
+ PosterURL: local.PosterURL,
+ BackdropURL: local.BackdropURL,
+ Year: local.Year,
+ Rating: local.Rating,
+ TMDbID: local.TMDbID,
+ NSFW: local.NSFW,
+ }
+ if local.Genres != "" {
+ match.Genres = splitNFOList(local.Genres)
+ }
+ if local.Countries != "" {
+ match.Countries = splitNFOList(local.Countries)
+ }
+ if local.Languages != "" {
+ match.Languages = splitNFOList(local.Languages)
+ }
+ return match
+}
+
+func organizeMetadataCacheKey(mediaType, query string, year int) string {
+ return strings.ToLower(strings.TrimSpace(mediaType)) + "|" + fmt.Sprint(year) + "|" + strings.ToLower(strings.TrimSpace(query))
+}
+
func (o *OrganizerService) smartClassifySourceFile(ctx context.Context, src, sourceRoot, mediaType, title, parsedTitle string) string {
if o == nil || !o.isSmartClassifyEnabled(ctx) {
return ""
diff --git a/internal/service/organizer_scrape_test.go b/internal/service/organizer_scrape_test.go
index 7d066c8..9689e86 100644
--- a/internal/service/organizer_scrape_test.go
+++ b/internal/service/organizer_scrape_test.go
@@ -69,6 +69,39 @@ func TestOrganizeDirectoryScanAndScrapeAfter(t *testing.T) {
}
}
+func TestOrganizeDirectoryUsesScraperMatchBeforeRename(t *testing.T) {
+ scraper, repos, closeServer := newTestScraper(t)
+ defer closeServer()
+
+ root := t.TempDir()
+ src := filepath.Join(root, "downloads")
+ dest := filepath.Join(root, "media")
+ sourceFile := filepath.Join(src, "Spy.x.Family.S01E01.2022.1080p.mkv")
+ writeOrgFile(t, sourceFile, "episode")
+
+ organizer := NewOrganizerService(&config.Config{}, zap.NewNop(), repos)
+ organizer.SetScraper(scraper)
+ res, err := organizer.OrganizeDirectory(t.Context(), OrganizeOptions{
+ SourcePath: src,
+ DestPath: dest,
+ TransferMode: TransferCopy,
+ MediaType: "tv",
+ })
+ if err != nil {
+ t.Fatalf("organize directory: %v", err)
+ }
+ if res.Organized != 1 {
+ t.Fatalf("organized = %d, want 1", res.Organized)
+ }
+ want := filepath.Join(dest, "电视剧", "间谍过家家", "Season 01", "间谍过家家 - S01E01.mkv")
+ if _, err := os.Stat(want); err != nil {
+ t.Fatalf("organized file should use matched metadata path %q: %v; items=%#v", want, err, res.Items)
+ }
+ if len(res.Items) != 1 || res.Items[0].Target != want || res.Items[0].Title != "间谍过家家" {
+ t.Fatalf("organize preview did not use scraper metadata: %#v", res.Items)
+ }
+}
+
func TestOrganizeScanAndScrapeRetriesNoMatchRows(t *testing.T) {
scraper, repos, closeServer := newTestScraper(t)
defer closeServer()
diff --git a/internal/service/service.go b/internal/service/service.go
index 5d78259..c4cdfb3 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -101,6 +101,7 @@ func New(cfg *config.Config, log *zap.Logger, repos *repository.Container) *Cont
scraper := NewScraperService(cfg, log, repos, tmdb, bangumi, thetvdb, fanart, hub, adult)
organizer := NewOrganizerService(cfg, log, repos)
organizer.SetProbe(probe)
+ organizer.SetScraper(scraper)
discover := NewDiscoverService(log, tmdb)
transcoder := NewTranscoderService(cfg, log, repos, hub)
scanner := NewScannerService(cfg, log, repos, hub, probe, scraper)
diff --git a/internal/service/telegram_api_test.go b/internal/service/telegram_api_test.go
index 026e3e8..7d57051 100644
--- a/internal/service/telegram_api_test.go
+++ b/internal/service/telegram_api_test.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
+ "fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/ShukeBta/MediaStationGo/internal/model"
+ "go.uber.org/zap"
)
func TestTelegramMethodURLUsesCustomAPIBase(t *testing.T) {
@@ -249,6 +251,150 @@ func TestTelegramReplyAutoDeletesSentMessage(t *testing.T) {
waitForTelegramMethod(t, requests, "deleteMessage")
}
+func TestTelegramGroupCommandSendsPanelPrivately(t *testing.T) {
+ var payloads []struct {
+ ChatID any `json:"chat_id"`
+ Text string `json:"text"`
+ ReplyMarkup map[string]any `json:"reply_markup"`
+ }
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !strings.HasSuffix(r.URL.Path, "/sendMessage") {
+ http.NotFound(w, r)
+ return
+ }
+ var payload struct {
+ ChatID any `json:"chat_id"`
+ Text string `json:"text"`
+ ReplyMarkup map[string]any `json:"reply_markup"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+ t.Fatalf("decode sendMessage: %v", err)
+ }
+ payloads = append(payloads, payload)
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":777}}`))
+ }))
+ defer server.Close()
+
+ repos, bot := newBotTestService(t)
+ cfg, _ := json.Marshal(map[string]string{
+ "bot_token": "123456:ABC-def",
+ "api_base_url": server.URL,
+ "group_chat_id": "-100123",
+ "auto_delete_seconds": "-1",
+ })
+ if err := repos.DB.Create(&model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: string(cfg)}).Error; err != nil {
+ t.Fatalf("create channel: %v", err)
+ }
+ update, _ := json.Marshal(TelegramUpdate{
+ UpdateID: 1,
+ Message: &TelegramMessage{
+ MessageID: 55,
+ From: TelegramUser{ID: 9002, Username: "viewer", FirstName: "Viewer"},
+ Chat: TelegramChat{ID: -100123, Type: "supergroup"},
+ Text: "/menu",
+ },
+ })
+ if err := bot.HandleWebhook(t.Context(), update); err != nil {
+ t.Fatalf("handle webhook: %v", err)
+ }
+ if len(payloads) != 2 {
+ t.Fatalf("sendMessage count = %d, payloads=%#v", len(payloads), payloads)
+ }
+ if got := fmt.Sprint(payloads[0].ChatID); got != "9002" {
+ t.Fatalf("first message should be private to requester, chat_id=%s payload=%#v", got, payloads[0])
+ }
+ if payloads[0].ReplyMarkup == nil {
+ t.Fatalf("private panel should include inline keyboard: %#v", payloads[0])
+ }
+ if got := fmt.Sprint(payloads[1].ChatID); got != "-100123" {
+ t.Fatalf("second message should be group ack, chat_id=%s payload=%#v", got, payloads[1])
+ }
+ if payloads[1].ReplyMarkup != nil {
+ t.Fatalf("group ack must not expose buttons: %#v", payloads[1])
+ }
+ if !strings.Contains(payloads[1].Text, "私聊") {
+ t.Fatalf("group ack should explain private delivery, got %q", payloads[1].Text)
+ }
+}
+
+func TestTelegramGroupCallbackIsRejected(t *testing.T) {
+ var callbackPayloads []struct {
+ CallbackID string `json:"callback_query_id"`
+ Text string `json:"text"`
+ ShowAlert bool `json:"show_alert"`
+ }
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !strings.HasSuffix(r.URL.Path, "/answerCallbackQuery") {
+ http.NotFound(w, r)
+ return
+ }
+ var payload struct {
+ CallbackID string `json:"callback_query_id"`
+ Text string `json:"text"`
+ ShowAlert bool `json:"show_alert"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+ t.Fatalf("decode answerCallbackQuery: %v", err)
+ }
+ callbackPayloads = append(callbackPayloads, payload)
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"ok":true,"result":true}`))
+ }))
+ defer server.Close()
+
+ ctx := t.Context()
+ repos, auth, _, _ := newAuthTestServices(t)
+ user, _, err := auth.Register(ctx, "viewer", "secret-pass")
+ if err != nil {
+ t.Fatalf("register user: %v", err)
+ }
+ if err := repos.DB.AutoMigrate(&model.NotifyChannel{}); err != nil {
+ t.Fatalf("migrate notify channel: %v", err)
+ }
+ if err := repos.DB.Create(&model.TelegramBinding{
+ TelegramUserID: 9002,
+ TelegramName: "@viewer",
+ ChatID: 9002,
+ UserID: user.ID,
+ }).Error; err != nil {
+ t.Fatalf("create binding: %v", err)
+ }
+ cfg, _ := json.Marshal(map[string]string{
+ "bot_token": "123456:ABC-def",
+ "api_base_url": server.URL,
+ "group_chat_id": "-100123",
+ "auto_delete_seconds": "-1",
+ })
+ if err := repos.DB.Create(&model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: string(cfg)}).Error; err != nil {
+ t.Fatalf("create channel: %v", err)
+ }
+ before, _ := repos.User.FindByID(ctx, user.ID)
+ bot := NewTelegramBotService(zap.NewNop(), repos, nil, auth)
+ update, _ := json.Marshal(TelegramUpdate{
+ UpdateID: 2,
+ CallbackQuery: &TelegramCallbackQuery{
+ ID: "cb-group",
+ From: TelegramUser{ID: 9002, Username: "viewer", FirstName: "Viewer"},
+ Message: &TelegramMessage{MessageID: 56, Chat: TelegramChat{ID: -100123, Type: "supergroup"}},
+ Data: "adult_toggle",
+ },
+ })
+ if err := bot.HandleWebhook(ctx, update); err != nil {
+ t.Fatalf("handle webhook: %v", err)
+ }
+ if len(callbackPayloads) != 1 {
+ t.Fatalf("answerCallbackQuery count = %d", len(callbackPayloads))
+ }
+ if !callbackPayloads[0].ShowAlert || !strings.Contains(callbackPayloads[0].Text, "群组内按钮面板已禁用") {
+ t.Fatalf("unexpected callback answer: %#v", callbackPayloads[0])
+ }
+ after, _ := repos.User.FindByID(ctx, user.ID)
+ if before == nil || after == nil || before.HideAdult != after.HideAdult {
+ t.Fatalf("group callback should not mutate user adult visibility: before=%#v after=%#v", before, after)
+ }
+}
+
func waitForTelegramMethod(t *testing.T, requests <-chan string, want string) {
t.Helper()
deadline := time.After(2 * time.Second)
diff --git a/internal/service/telegram_bot.go b/internal/service/telegram_bot.go
index 54d414a..d3253f4 100644
--- a/internal/service/telegram_bot.go
+++ b/internal/service/telegram_bot.go
@@ -217,13 +217,13 @@ func (s *TelegramBotService) HandleWebhook(ctx context.Context, body []byte) err
reply, err := s.executeCommand(ctx, channel, msg, text)
if err != nil {
s.log.Error("command failed", zap.Error(err))
- _ = s.reply(ctx, channel, msg.Chat.ID, telegramCommandReply{Text: "命令执行失败: " + err.Error()})
+ _ = s.replyForMessage(ctx, channel, msg, telegramCommandReply{Text: "命令执行失败: " + err.Error()})
s.deleteTelegramSourceMessage(channel, msg.Chat.ID, msg.MessageID)
return nil
}
if reply.Text != "" {
- if err := s.reply(ctx, channel, msg.Chat.ID, reply); err != nil {
+ if err := s.replyForMessage(ctx, channel, msg, reply); err != nil {
s.log.Error("reply failed", zap.Error(err))
}
s.deleteTelegramSourceMessage(channel, msg.Chat.ID, msg.MessageID)
@@ -255,6 +255,15 @@ func telegramIsGroupChat(chatType string) bool {
return chatType != "" && chatType != "private"
}
+func telegramPrivateMessageForUser(msg *TelegramMessage) *TelegramMessage {
+ if msg == nil || !telegramIsGroupChat(msg.Chat.Type) {
+ return msg
+ }
+ copied := *msg
+ copied.Chat = TelegramChat{ID: msg.From.ID, Type: "private"}
+ return &copied
+}
+
func telegramGroupPrivateAdminHint() string {
return "管理命令请私聊 Bot 使用 /menu 或对应管理员命令,避免在群组公开管理面板。"
}
@@ -267,6 +276,14 @@ func telegramGroupPrivateUserHint(action string) string {
return action + "包含账号凭据或敏感信息,请私聊 Bot 操作;群组内仅开放账号状态、签到、设备与成人目录开关。"
}
+func telegramGroupPrivateDeliverySentHint() string {
+ return "已把你的 Bot 面板/执行结果私聊发送给你。若没收到,请先私聊 Bot 发送 /start。"
+}
+
+func telegramGroupPrivateDeliveryFailedHint() string {
+ return "无法私聊发送给你。请先打开 Bot 私聊窗口发送 /start,再回群里使用命令。"
+}
+
// cmdStart 处理 /start 命令。
func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply {
name := msg.From.FirstName
@@ -930,6 +947,29 @@ func (s *TelegramBotService) reply(ctx context.Context, channel *model.NotifyCha
return nil
}
+func (s *TelegramBotService) replyForMessage(ctx context.Context, channel *model.NotifyChannel, msg *TelegramMessage, reply telegramCommandReply) error {
+ if msg == nil {
+ return nil
+ }
+ if strings.TrimSpace(reply.Text) == "" {
+ return nil
+ }
+ if !telegramIsGroupChat(msg.Chat.Type) {
+ return s.reply(ctx, channel, msg.Chat.ID, reply)
+ }
+ if err := s.reply(ctx, channel, msg.From.ID, reply); err != nil {
+ if s.log != nil {
+ s.log.Warn("telegram private reply from group failed",
+ zap.Int("group_chat_id", msg.Chat.ID),
+ zap.Int("telegram_user_id", msg.From.ID),
+ zap.Error(sanitizeTelegramError(err)),
+ )
+ }
+ return s.reply(ctx, channel, msg.Chat.ID, telegramCommandReply{Text: telegramGroupPrivateDeliveryFailedHint()})
+ }
+ return s.reply(ctx, channel, msg.Chat.ID, telegramCommandReply{Text: telegramGroupPrivateDeliverySentHint()})
+}
+
func (s *TelegramBotService) deleteTelegramSourceMessage(channel *model.NotifyChannel, chatID, messageID int) {
if messageID <= 0 {
return
@@ -1057,6 +1097,15 @@ func (s *TelegramBotService) handleCallback(ctx context.Context, cb *TelegramCal
channel = s.findChannelByChatID(ctx, cb.Message.Chat.ID)
}
// 立即应答回调,关闭按钮上的加载状态,避免客户端长时间转圈。
+ if telegramIsGroupChat(cb.Message.Chat.Type) {
+ s.answerCallbackWithText(ctx, channel, cb.ID, "为了隐私,群组内按钮面板已禁用。请私聊 Bot 或在群里发送 /menu,我会把面板私聊给你。", true)
+ s.deleteTelegramSourceMessage(channel, cb.Message.Chat.ID, cb.Message.MessageID)
+ return nil
+ }
+ if cb.Message.Chat.Type == "private" && cb.Message.Chat.ID != cb.From.ID {
+ s.answerCallbackWithText(ctx, channel, cb.ID, "这个面板不属于你,请发送 /menu 打开自己的面板。", true)
+ return nil
+ }
s.answerCallback(ctx, channel, cb.ID)
data := strings.TrimSpace(cb.Data)
if data == "adult_toggle" {
@@ -1080,6 +1129,10 @@ func (s *TelegramBotService) handleCallback(ctx context.Context, cb *TelegramCal
// answerCallback 应答 Telegram 回调查询,关闭按钮上的加载提示。
func (s *TelegramBotService) answerCallback(ctx context.Context, channel *model.NotifyChannel, callbackID string) {
+ s.answerCallbackWithText(ctx, channel, callbackID, "", false)
+}
+
+func (s *TelegramBotService) answerCallbackWithText(ctx context.Context, channel *model.NotifyChannel, callbackID, text string, showAlert bool) {
if channel == nil || strings.TrimSpace(callbackID) == "" {
return
}
@@ -1087,9 +1140,14 @@ func (s *TelegramBotService) answerCallback(ctx context.Context, channel *model.
if strings.TrimSpace(cfg["bot_token"]) == "" {
return
}
- if err := telegramPostJSON(ctx, cfg, "answerCallbackQuery", map[string]interface{}{
+ payload := map[string]interface{}{
"callback_query_id": callbackID,
- }, 8*time.Second); err != nil {
+ }
+ if strings.TrimSpace(text) != "" {
+ payload["text"] = text
+ payload["show_alert"] = showAlert
+ }
+ if err := telegramPostJSON(ctx, cfg, "answerCallbackQuery", payload, 8*time.Second); err != nil {
s.log.Debug("telegram answerCallbackQuery failed", zap.Error(sanitizeTelegramError(err)))
}
}
diff --git a/internal/service/telegram_commands.go b/internal/service/telegram_commands.go
index 5d51b57..0bd0c19 100644
--- a/internal/service/telegram_commands.go
+++ b/internal/service/telegram_commands.go
@@ -25,11 +25,13 @@ func (s *TelegramBotService) telegramCommandDefinitions(ctx context.Context, cha
return []telegramCommandDefinition{
{Aliases: []string{"/start"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) {
if len(args) == 0 {
- return s.mainMenu(ctx, channel, msg), nil
+ return s.mainMenu(ctx, channel, telegramPrivateMessageForUser(msg)), nil
}
return s.cmdStart(ctx, msg, args), nil
}},
- {Aliases: []string{"/menu"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.mainMenu(ctx, channel, msg), nil }},
+ {Aliases: []string{"/menu"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) {
+ return s.mainMenu(ctx, channel, telegramPrivateMessageForUser(msg)), nil
+ }},
{Aliases: []string{"/cancel"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) {
s.takePending(int64(msg.From.ID))
return telegramCommandReply{Text: "已取消当前操作。"}, nil