feat: add .chatid variable in file name template

This commit is contained in:
krau
2025-09-30 21:34:35 +08:00
parent f947ee6fc7
commit 4d837e946c
5 changed files with 202 additions and 183 deletions

View File

@@ -119,7 +119,9 @@ func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
- {{.msgtags}}: 消息中的标签, 将以下划线分隔输出
- {{.msggen}}: 根据消息生成的文件名
- {{.msgdate}}: 消息日期, 格式 YYYY-MM-DD_HH-MM-SS
- {{.origname}}: 媒体的原始文件名 (如果有)`
- {{.origname}}: 媒体的原始文件名 (如果有)
- {{.chatid}}: 消息的聊天ID
`
if user.FilenameTemplate != "" {
text += fmt.Sprintf("\n\n当前模板: %s", user.FilenameTemplate)
}

View File

@@ -1,33 +1,17 @@
package handlers
import (
"fmt"
"path"
"regexp"
"strings"
"text/template"
"time"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/dispatcher/handlers"
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
"github.com/celestix/gotgproto/ext"
"github.com/charmbracelet/log"
sabotfilters "github.com/krau/SaveAny-Bot/client/bot/handlers/utils/filters"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
userclient "github.com/krau/SaveAny-Bot/client/user"
"github.com/krau/SaveAny-Bot/common/utils/strutil"
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/core/tasks/tfile"
"github.com/krau/SaveAny-Bot/database"
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
"github.com/krau/SaveAny-Bot/storage"
"github.com/rs/xid"
)
type DescCommandHandler struct {
@@ -76,135 +60,3 @@ func Register(disp dispatcher.Dispatcher) {
go listenMediaMessageEvent(userclient.GetMediaMessageCh())
}
}
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
logger := log.FromContext(userclient.GetCtx())
for event := range ch {
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
ctx := event.Ctx
file := event.File
chats, err := database.GetWatchChatsByChatID(ctx, event.ChatID)
if err != nil {
logger.Errorf("Failed to get watch chats for chat ID %d: %v", event.ChatID, err)
continue
}
msgText := event.File.Message().GetMessage()
for _, chat := range chats {
if chat.Filter != "" {
filter := strings.Split(chat.Filter, ":")
if len(filter) != 2 {
logger.Warnf("Invalid filter format in chat %d, skipping", chat.ChatID)
continue
}
filterType := filter[0]
filterData := filter[1]
switch filterType {
case "msgre": // [TODO] enums for filter types
if ok, err := regexp.MatchString(filterData, msgText); err != nil {
continue
} else if !ok {
continue
}
default:
logger.Warnf("Unsupported filter type %s in chat %d, skipping", filterType, chat.ChatID)
continue
}
}
user, err := database.GetUserByID(ctx, chat.UserID)
if err != nil {
logger.Errorf("Failed to get user by ID %d: %v", chat.UserID, err)
continue
}
if user.DefaultStorage == "" {
logger.Warnf("User %d has no default storage set, skipping media message handling", chat.UserID)
continue
}
stor, err := storage.GetStorageByUserIDAndName(ctx, user.ChatID, user.DefaultStorage)
if err != nil {
logger.Errorf("Failed to get storage by user ID %d and name %s: %v", user.ChatID, user.DefaultStorage, err)
continue
}
switch user.FilenameStrategy {
case fnamest.Message.String():
file.SetName(tgutil.GenFileNameFromMessage(*file.Message()))
case fnamest.Template.String():
if user.FilenameTemplate == "" {
logger.Warnf("Empty filename template for user %d, using default filename", user.ChatID)
break
}
// [TODO] refactor this
message := file.Message()
tmpl, err := template.New("filename").Parse(user.FilenameTemplate)
if err != nil {
logger.Errorf("Failed to parse filename template for user %d: %s", user.ChatID, err)
break
}
data := mediautil.FilenameTemplateData{
MsgID: func() string {
id := message.GetID()
if id == 0 {
return ""
}
return fmt.Sprintf("%d", id)
}(),
MsgTags: func() string {
tags := strutil.ExtractTagsFromText(message.GetMessage())
if len(tags) == 0 {
return ""
}
return strings.Join(tags, "_")
}(),
MsgGen: tgutil.GenFileNameFromMessage(*message),
OrigName: func() string {
f, _ := tgutil.GetMediaFileName(message.Media)
return f
}(),
MsgDate: func() string {
date := message.GetDate()
if date == 0 {
return ""
}
t := time.Unix(int64(date), 0)
return t.Format("2006-01-02_15-04-05")
}(),
}.ToMap()
var sb strings.Builder
err = tmpl.Execute(&sb, data)
if err != nil {
log.FromContext(ctx).Errorf("failed to execute filename template: %s", err)
break
}
file.SetName(sb.String())
}
var dirPath string
if user.ApplyRule && user.Rules != nil {
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
if !matched {
goto startCreateTask
}
dirPath = matchedDirPath.String()
if matchedStorageName.IsUsable() {
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
if err != nil {
logger.Errorf("Failed to get storage by user ID and name: %s", err)
continue
}
}
}
startCreateTask:
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name()))
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
taskid := xid.New().String()
task, err := tfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
if err != nil {
logger.Errorf("create task failed: %s", err)
continue
}
if err := core.AddTask(injectCtx, task); err != nil {
logger.Errorf("add task failed: %s", err)
continue
}
logger.Infof("Added media message task for user %d in chat %d: %s", chat.UserID, event.ChatID, file.Name())
}
}
}

View File

@@ -26,11 +26,13 @@ func IsSupported(media tg.MessageMediaClass) bool {
}
type FilenameTemplateData struct {
MsgID string `json:"msgid,omitempty"`
MsgTags string `json:"msgtags,omitempty"`
MsgGen string `json:"msggen,omitempty"`
MsgDate string `json:"msgdate,omitempty"`
OrigName string `json:"origname,omitempty"`
MsgID string `json:"msgid,omitempty"`
MsgTags string `json:"msgtags,omitempty"`
MsgGen string `json:"msggen,omitempty"`
MsgDate string `json:"msgdate,omitempty"`
OrigName string `json:"origname,omitempty"`
ChatID string `json:"chatid,omitempty"`
ChatTitle string `json:"chattitle,omitempty"`
}
func (f FilenameTemplateData) ToMap() map[string]string {
@@ -40,6 +42,7 @@ func (f FilenameTemplateData) ToMap() map[string]string {
"msggen": f.MsgGen,
"msgdate": f.MsgDate,
"origname": f.OrigName,
"chatid": f.ChatID,
}
}
@@ -61,35 +64,7 @@ func TfileOptions(ctx context.Context, user *database.User, message *tg.Message)
fnameOpt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message))
break
}
data := FilenameTemplateData{
MsgID: func() string {
id := message.GetID()
if id == 0 {
return ""
}
return fmt.Sprintf("%d", id)
}(),
MsgTags: func() string {
tags := strutil.ExtractTagsFromText(message.GetMessage())
if len(tags) == 0 {
return ""
}
return strings.Join(tags, "_")
}(),
MsgGen: tgutil.GenFileNameFromMessage(*message),
OrigName: func() string {
f, _ := tgutil.GetMediaFileName(message.Media)
return f
}(),
MsgDate: func() string {
date := message.GetDate()
if date == 0 {
return ""
}
t := time.Unix(int64(date), 0)
return t.Format("2006-01-02_15-04-05")
}(),
}.ToMap()
data := BuildFilenameTemplateData(message)
var sb strings.Builder
err = tmpl.Execute(&sb, data)
if err != nil {
@@ -104,3 +79,64 @@ func TfileOptions(ctx context.Context, user *database.User, message *tg.Message)
opts = append(opts, fnameOpt, tfile.WithMessage(message))
return opts
}
func BuildFilenameTemplateData(message *tg.Message) map[string]string {
data := FilenameTemplateData{
MsgID: func() string {
id := message.GetID()
if id == 0 {
return ""
}
return fmt.Sprintf("%d", id)
}(),
MsgTags: func() string {
tags := strutil.ExtractTagsFromText(message.GetMessage())
if len(tags) == 0 {
return ""
}
return strings.Join(tags, "_")
}(),
MsgGen: tgutil.GenFileNameFromMessage(*message),
OrigName: func() string {
f, _ := tgutil.GetMediaFileName(message.Media)
return f
}(),
MsgDate: func() string {
date := message.GetDate()
if date == 0 {
return ""
}
t := time.Unix(int64(date), 0)
return t.Format("2006-01-02_15-04-05")
}(),
ChatID: func() string {
// 如果消息是频道的(从消息链接中fetch的) 直接使用其chat id, 无论它是否是从其他来源转发的
if message.GetPost() {
peer := message.GetPeerID()
switch p := peer.(type) {
case *tg.PeerChannel:
return intToStringOmitZero(p.ChannelID)
default: // impossible case
return intToStringOmitZero(tgutil.ChatIdFromPeer(peer))
}
}
fwdHeader, ok := message.GetFwdFrom()
if !ok {
return intToStringOmitZero(tgutil.ChatIdFromPeer(message.GetPeerID()))
}
fwdFrom, ok := fwdHeader.GetFromID()
if !ok {
return intToStringOmitZero(tgutil.ChatIdFromPeer(message.GetPeerID()))
}
return intToStringOmitZero(tgutil.ChatIdFromPeer(fwdFrom))
}(),
}.ToMap()
return data
}
func intToStringOmitZero(i int64) string {
if i == 0 {
return ""
}
return fmt.Sprintf("%d", i)
}

View File

@@ -1,16 +1,26 @@
package handlers
import (
"path"
"regexp"
"strings"
"text/template"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
"github.com/charmbracelet/log"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
userclient "github.com/krau/SaveAny-Bot/client/user"
"github.com/krau/SaveAny-Bot/common/i18n"
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/core/tasks/tfile"
"github.com/krau/SaveAny-Bot/database"
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
"github.com/krau/SaveAny-Bot/storage"
"github.com/rs/xid"
)
func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
@@ -109,3 +119,106 @@ func handleUnwatchCmd(ctx *ext.Context, update *ext.Update) error {
ctx.Reply(update, ext.ReplyTextString("已取消监听聊天: "+chatArg), nil)
return dispatcher.EndGroups
}
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
logger := log.FromContext(userclient.GetCtx())
for event := range ch {
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
ctx := event.Ctx
file := event.File
chats, err := database.GetWatchChatsByChatID(ctx, event.ChatID)
if err != nil {
logger.Errorf("Failed to get watch chats for chat ID %d: %v", event.ChatID, err)
continue
}
msgText := event.File.Message().GetMessage()
for _, chat := range chats {
if chat.Filter != "" {
filter := strings.Split(chat.Filter, ":")
if len(filter) != 2 {
logger.Warnf("Invalid filter format in chat %d, skipping", chat.ChatID)
continue
}
filterType := filter[0]
filterData := filter[1]
switch filterType {
case "msgre": // [TODO] enums for filter types
if ok, err := regexp.MatchString(filterData, msgText); err != nil {
continue
} else if !ok {
continue
}
default:
logger.Warnf("Unsupported filter type %s in chat %d, skipping", filterType, chat.ChatID)
continue
}
}
user, err := database.GetUserByID(ctx, chat.UserID)
if err != nil {
logger.Errorf("Failed to get user by ID %d: %v", chat.UserID, err)
continue
}
if user.DefaultStorage == "" {
logger.Warnf("User %d has no default storage set, skipping media message handling", chat.UserID)
continue
}
stor, err := storage.GetStorageByUserIDAndName(ctx, user.ChatID, user.DefaultStorage)
if err != nil {
logger.Errorf("Failed to get storage by user ID %d and name %s: %v", user.ChatID, user.DefaultStorage, err)
continue
}
switch user.FilenameStrategy {
case fnamest.Message.String():
file.SetName(tgutil.GenFileNameFromMessage(*file.Message()))
case fnamest.Template.String():
if user.FilenameTemplate == "" {
logger.Warnf("Empty filename template for user %d, using default filename", user.ChatID)
break
}
message := file.Message()
tmpl, err := template.New("filename").Parse(user.FilenameTemplate)
if err != nil {
logger.Errorf("Failed to parse filename template for user %d: %s", user.ChatID, err)
break
}
data := mediautil.BuildFilenameTemplateData(message)
var sb strings.Builder
err = tmpl.Execute(&sb, data)
if err != nil {
log.FromContext(ctx).Errorf("failed to execute filename template: %s", err)
break
}
file.SetName(sb.String())
}
var dirPath string
if user.ApplyRule && user.Rules != nil {
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
if !matched {
goto startCreateTask
}
dirPath = matchedDirPath.String()
if matchedStorageName.IsUsable() {
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
if err != nil {
logger.Errorf("Failed to get storage by user ID and name: %s", err)
continue
}
}
}
startCreateTask:
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name()))
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
taskid := xid.New().String()
task, err := tfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
if err != nil {
logger.Errorf("create task failed: %s", err)
continue
}
if err := core.AddTask(injectCtx, task); err != nil {
logger.Errorf("add task failed: %s", err)
continue
}
logger.Infof("Added media message task for user %d in chat %d: %s", chat.UserID, event.ChatID, file.Name())
}
}
}