Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71844deab1 | ||
|
|
55fed6389e | ||
|
|
8ce5c2e007 | ||
|
|
6ecfbd8385 | ||
|
|
6c2bfd72cd | ||
|
|
8ea5be5b90 | ||
|
|
7f483056e0 | ||
|
|
a6f88d7f75 | ||
|
|
b757df0b5e | ||
|
|
b017046c8b |
38
.github/workflows/build-docker.yml
vendored
38
.github/workflows/build-docker.yml
vendored
@@ -20,19 +20,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -50,11 +37,36 @@ jobs:
|
||||
org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot
|
||||
org.opencontainers.image.url=https://github.com/krau/SaveAny-Bot
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from Git Ref
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=$(echo "${{ github.ref }}" | sed 's/refs\/tags\/v//')
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
GitCommit=${{ github.sha }}
|
||||
BuildTime=${{ format(github.event.repository.updated_at, 'yyyy-MM-dd HH:mm:ss') }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
23
Dockerfile
23
Dockerfile
@@ -1,24 +1,27 @@
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
ARG VERSION="dev"
|
||||
ARG GitCommit="Unknown"
|
||||
ARG BuildTime="Unknown"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/krau/SaveAny-Bot/common.Version=Docker" -o saveany-bot .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
CGO_ENABLED=0 \
|
||||
go build -trimpath \
|
||||
-ldflags "-s -w \
|
||||
-X github.com/krau/SaveAny-Bot/common.Version=${VERSION} \
|
||||
-X github.com/krau/SaveAny-Bot/common.GitCommit=${GiTCommit} \
|
||||
-X github.com/krau/SaveAny-Bot/common.BuildTime=${BuildTime}" \
|
||||
-o saveany-bot .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN addgroup -S saveany && adduser -S saveany -G saveany
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/saveany-bot .
|
||||
|
||||
RUN mkdir -p /app/data /app/downloads /app/cache && \
|
||||
chown -R saveany:saveany /app /app/downloads /app/cache /app/data
|
||||
|
||||
RUN chmod +x /app/saveany-bot
|
||||
|
||||
USER saveany
|
||||
|
||||
ENTRYPOINT ["/app/saveany-bot"]
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
@@ -28,11 +27,11 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
storages := storage.GetUserStorages(user.ChatID)
|
||||
if len(storages) == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
// storages := storage.GetUserStorages(user.ChatID)
|
||||
// if len(storages) == 0 {
|
||||
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
// return dispatcher.EndGroups
|
||||
// }
|
||||
|
||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
@@ -19,47 +19,47 @@ var (
|
||||
linkRegex = regexp.MustCompile(linkRegexString)
|
||||
)
|
||||
|
||||
func parseLink(ctx *ext.Context, link string) (chatID int64, messageID int, err error) {
|
||||
strSlice := strings.Split(link, "/")
|
||||
if len(strSlice) < 3 {
|
||||
return 0, 0, fmt.Errorf("链接格式错误: %s", link)
|
||||
}
|
||||
messageID, err = strconv.Atoi(strSlice[len(strSlice)-1])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("无法解析消息 ID: %s", err)
|
||||
}
|
||||
if len(strSlice) == 3 {
|
||||
chatUsername := strSlice[1]
|
||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("解析用户名失败: %s", err)
|
||||
}
|
||||
if linkChat == nil {
|
||||
return 0, 0, fmt.Errorf("找不到该聊天: %s", chatUsername)
|
||||
}
|
||||
chatID = linkChat.GetID()
|
||||
} else if len(strSlice) == 4 {
|
||||
chatIDInt, err := strconv.Atoi(strSlice[2])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("无法解析 Chat ID: %s", err)
|
||||
}
|
||||
chatID = int64(chatIDInt)
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("无效的链接: %s", link)
|
||||
}
|
||||
return chatID, messageID, nil
|
||||
}
|
||||
|
||||
func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
common.Log.Trace("Got link message")
|
||||
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
||||
if link == "" {
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
strSlice := strings.Split(link, "/")
|
||||
if len(strSlice) < 3 {
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
messageID, err := strconv.Atoi(strSlice[len(strSlice)-1])
|
||||
linkChatID, messageID, err := parseLink(ctx, link)
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析消息 ID 失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
var linkChatID int64
|
||||
if len(strSlice) == 3 {
|
||||
chatUsername := strSlice[1]
|
||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析用户名失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析用户名失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if linkChat == nil {
|
||||
common.Log.Errorf("无法找到聊天: %s", chatUsername)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
linkChatID = linkChat.GetID()
|
||||
} else if len(strSlice) == 4 {
|
||||
chatID, err := strconv.Atoi(strSlice[2])
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析 Chat ID 失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析 Chat ID 失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
linkChatID = int64(chatID)
|
||||
} else {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法解析链接"), nil)
|
||||
common.Log.Errorf("解析链接失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析链接失败: "+err.Error()), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -69,12 +69,13 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
storages := storage.GetUserStorages(user.ChatID)
|
||||
|
||||
if len(storages) == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
// storages := storage.GetUserStorages(user.ChatID)
|
||||
// if len(storages) == 0 {
|
||||
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
// return dispatcher.EndGroups
|
||||
// }
|
||||
|
||||
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
||||
if err != nil {
|
||||
common.Log.Errorf("回复失败: %s", err)
|
||||
|
||||
@@ -63,12 +63,11 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
storages := storage.GetUserStorages(user.ChatID)
|
||||
|
||||
if len(storages) == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
// storages := storage.GetUserStorages(user.ChatID)
|
||||
// if len(storages) == 0 {
|
||||
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
// return dispatcher.EndGroups
|
||||
// }
|
||||
|
||||
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
|
||||
if err != nil {
|
||||
|
||||
95
bot/handle_send.go
Normal file
95
bot/handle_send.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
tgtypes "github.com/celestix/gotgproto/types"
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
func copyMediaToChat(ctx *ext.Context, msg *tg.Message, chatID int64) (*tgtypes.Message, error) {
|
||||
media, ok := msg.GetMedia()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("获取媒体失败")
|
||||
}
|
||||
|
||||
req := &tg.MessagesSendMediaRequest{
|
||||
InvertMedia: msg.InvertMedia,
|
||||
Message: msg.Message,
|
||||
}
|
||||
|
||||
switch m := media.(type) {
|
||||
case *tg.MessageMediaDocument:
|
||||
document, ok := m.Document.AsNotEmpty()
|
||||
if !ok {
|
||||
return nil, ErrEmptyDocument
|
||||
}
|
||||
inputMedia := &tg.InputMediaDocument{
|
||||
ID: document.AsInput(),
|
||||
}
|
||||
inputMedia.SetFlags()
|
||||
req.Media = inputMedia
|
||||
|
||||
case *tg.MessageMediaPhoto:
|
||||
photo, ok := m.Photo.AsNotEmpty()
|
||||
if !ok {
|
||||
return nil, ErrEmptyPhoto
|
||||
}
|
||||
inputMedia := &tg.InputMediaPhoto{
|
||||
ID: photo.AsInput(),
|
||||
}
|
||||
inputMedia.SetFlags()
|
||||
req.Media = inputMedia
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的媒体类型: %T", media)
|
||||
}
|
||||
|
||||
req.SetEntities(msg.Entities)
|
||||
req.SetFlags()
|
||||
|
||||
return ctx.SendMedia(chatID, req)
|
||||
}
|
||||
|
||||
func sendFileToTelegram(ctx *ext.Context, update *ext.Update) error {
|
||||
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||
if len(args) < 3 {
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
QueryID: update.CallbackQuery.QueryID,
|
||||
Alert: true,
|
||||
Message: "参数错误",
|
||||
CacheTime: 5,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
fileChatID, _ := strconv.Atoi(args[1])
|
||||
fileMessageID, _ := strconv.Atoi(args[2])
|
||||
fileMessage, err := GetTGMessage(ctx, int64(fileChatID), fileMessageID)
|
||||
if err != nil {
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
QueryID: update.CallbackQuery.QueryID,
|
||||
Alert: true,
|
||||
Message: "无法获取文件消息",
|
||||
CacheTime: 5,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
_, err = copyMediaToChat(ctx, fileMessage, update.EffectiveChat().GetID())
|
||||
if err != nil {
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
QueryID: update.CallbackQuery.QueryID,
|
||||
Alert: true,
|
||||
Message: fmt.Sprintf("发送文件失败: %s", err),
|
||||
CacheTime: 5,
|
||||
})
|
||||
} else {
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
QueryID: update.CallbackQuery.QueryID,
|
||||
})
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
@@ -29,5 +29,6 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
|
||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage))
|
||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("cancel"), cancelTask))
|
||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("send_here"), sendFileToTelegram))
|
||||
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gotd/contrib/middleware/floodwait"
|
||||
"github.com/gotd/contrib/middleware/ratelimit"
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
@@ -30,8 +31,40 @@ const noPermissionText string = `
|
||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||
userID := update.GetUserChat().GetID()
|
||||
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
||||
if config.Cfg.AsPublicCopyMediaBot {
|
||||
tryCopyMedia(ctx, update)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
|
||||
func tryCopyMedia(ctx *ext.Context, update *ext.Update) {
|
||||
if !config.Cfg.AsPublicCopyMediaBot {
|
||||
return
|
||||
}
|
||||
if update.EffectiveMessage == nil || update.EffectiveMessage.Message == nil {
|
||||
return
|
||||
}
|
||||
common.Log.Tracef("Got copy media request from %d", update.EffectiveChat().GetID())
|
||||
msg := update.EffectiveMessage.Message
|
||||
if link := linkRegex.FindString(update.EffectiveMessage.Text); link != "" {
|
||||
linkChatID, messageID, err := parseLink(ctx, link)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fileMessage, err := GetTGMessage(ctx, linkChatID, messageID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if fileMessage == nil || fileMessage.Media == nil {
|
||||
return
|
||||
}
|
||||
msg = fileMessage
|
||||
}
|
||||
if _, err := copyMediaToChat(ctx, msg, update.EffectiveChat().GetID()); err != nil {
|
||||
common.Log.Errorf("Failed to copy media: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
16
bot/utils.go
16
bot/utils.go
@@ -49,9 +49,9 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*t
|
||||
return nil, fmt.Errorf("failed to get user by chat ID: %d, error: %w", userChatID, err)
|
||||
}
|
||||
storages := storage.GetUserStorages(user.ChatID)
|
||||
if len(storages) == 0 {
|
||||
return nil, ErrNoStorages
|
||||
}
|
||||
// if len(storages) == 0 {
|
||||
// return nil, ErrNoStorages
|
||||
// }
|
||||
|
||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||
for _, storage := range storages {
|
||||
@@ -71,6 +71,14 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*t
|
||||
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||
markup.Rows = append(markup.Rows, row)
|
||||
}
|
||||
markup.Rows = append(markup.Rows, tg.KeyboardButtonRow{
|
||||
Buttons: []tg.KeyboardButtonClass{
|
||||
&tg.KeyboardButtonCallback{
|
||||
Text: "发送到当前聊天",
|
||||
Data: []byte(fmt.Sprintf("send_here %d %d", fileChatID, fileMessageID)),
|
||||
},
|
||||
},
|
||||
})
|
||||
return markup, nil
|
||||
}
|
||||
|
||||
@@ -208,7 +216,7 @@ func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, e
|
||||
if err == nil {
|
||||
return cacheMessage, nil
|
||||
}
|
||||
common.Log.Debugf("Fetching message: %d", messageID)
|
||||
common.Log.Debugf("Fetching message: %d:%d", chatId, messageID)
|
||||
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -17,6 +17,9 @@ type Config struct {
|
||||
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
||||
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
||||
|
||||
// Experimental: 将拷贝媒体文件的功能设为公开可用
|
||||
AsPublicCopyMediaBot bool `toml:"as_public_copy_media_bot" mapstructure:"as_public_copy_media_bot" json:"as_public_copy_media_bot"`
|
||||
|
||||
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||
|
||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||
|
||||
46
docs/docs/experimental.md
Normal file
46
docs/docs/experimental.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 实验性功能
|
||||
|
||||
这里的功能不太稳定, 且未来可能会被删除或修改。
|
||||
|
||||
## 存储规则
|
||||
|
||||
允许你为 Bot 在上传文件到存储时设置一些重定向规则, 用于自动整理所保存的文件.
|
||||
|
||||
见: https://github.com/krau/SaveAny-Bot/issues/28
|
||||
|
||||
目前支持的规则类型:
|
||||
|
||||
1. FILENAME-REGEX
|
||||
2. MESSAGE-REGEX
|
||||
|
||||
添加规则的基本语法:
|
||||
|
||||
"规则类型 规则内容 存储名 路径"
|
||||
|
||||
注意空格的使用, 语法正确 bot 才能解析, 以下是一条合法的添加规则命令:
|
||||
|
||||
```
|
||||
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
此外, 规则中的存储名若使用 "CHOSEN" , 则表示存储到点击按钮选择的存储端的路径下
|
||||
|
||||
规则介绍:
|
||||
|
||||
### FILENAME-REGEX
|
||||
|
||||
根据文件名正则匹配, 规则内容要求为一个合法的正则表达式, 如
|
||||
|
||||
```
|
||||
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
表示将文件名后缀为 mp4,mkv,ts,avi,flv 的文件放到名为 MyAlist 存储下的 /视频 目录内 (同时受配置文件中的 `base_path` 影响)
|
||||
|
||||
### MESSAGE-REGEX
|
||||
|
||||
同上, 根据消息文本内容正则匹配
|
||||
|
||||
## 复制并发送媒体消息
|
||||
|
||||
将接收到的文件(媒体)消息, 或链接对应的消息原样发送到当前聊天, 点击选择存储按钮中的 "发送到当前聊天" 即可.
|
||||
@@ -35,4 +35,4 @@ Bot 接受两种消息: 文件和链接.
|
||||
|
||||
**不支持** Stream 模式的存储端:
|
||||
|
||||
- alist
|
||||
- alist
|
||||
@@ -30,5 +30,6 @@ nav:
|
||||
- index.md
|
||||
- deploy.md
|
||||
- help.md
|
||||
- experimental.md
|
||||
- faq.md
|
||||
- contribute.md
|
||||
|
||||
Reference in New Issue
Block a user