mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-07-09 14:31:25 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c56892c3 | ||
|
|
015539c009 | ||
|
|
71844deab1 | ||
|
|
55fed6389e | ||
|
|
8ce5c2e007 | ||
|
|
6ecfbd8385 | ||
|
|
6c2bfd72cd | ||
|
|
8ea5be5b90 | ||
|
|
7f483056e0 | ||
|
|
a6f88d7f75 | ||
|
|
b757df0b5e | ||
|
|
b017046c8b | ||
|
|
a474fdf6ae | ||
|
|
729e688748 | ||
|
|
9ea4857cd9 | ||
|
|
8bf7bc0e85 | ||
|
|
26e344a6f6 | ||
|
|
8f0744077e | ||
|
|
ed99a37831 | ||
|
|
488d709d85 | ||
|
|
66454b082a | ||
|
|
70e83e62d9 | ||
|
|
d2ddb9193a | ||
|
|
5f78db90c7 | ||
|
|
c3a4702e79 |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
*.md
|
||||||
|
.git
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
downloads/
|
||||||
|
data/
|
||||||
|
cache/
|
||||||
|
docs
|
||||||
|
config.example.toml
|
||||||
|
docker-compose.*
|
||||||
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: "👾 报告 bug"
|
name: "👾 报告 bug"
|
||||||
description: "报告 bug"
|
description: "报告 bug"
|
||||||
labels:
|
labels:
|
||||||
- ":space_invader: Bug"
|
- "bug"
|
||||||
assignees:
|
assignees:
|
||||||
- krau
|
- krau
|
||||||
body:
|
body:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
2
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: "⭐️ 功能请求"
|
name: "⭐️ 功能请求"
|
||||||
description: "功能请求"
|
description: "功能请求"
|
||||||
labels:
|
labels:
|
||||||
- ":fire: Enhancement"
|
- "enhancement"
|
||||||
assignees:
|
assignees:
|
||||||
- krau
|
- krau
|
||||||
body:
|
body:
|
||||||
|
|||||||
38
.github/workflows/build-docker.yml
vendored
38
.github/workflows/build-docker.yml
vendored
@@ -20,19 +20,6 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -50,11 +37,36 @@ jobs:
|
|||||||
org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot
|
org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot
|
||||||
org.opencontainers.image.url=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
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
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
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -1,13 +1,22 @@
|
|||||||
FROM golang:alpine AS builder
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG GitCommit="Unknown"
|
||||||
|
ARG BuildTime="Unknown"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.* ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -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
|
FROM alpine:latest
|
||||||
|
|
||||||
@@ -15,4 +24,4 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY --from=builder /app/saveany-bot .
|
COPY --from=builder /app/saveany-bot .
|
||||||
|
|
||||||
CMD ["./saveany-bot"]
|
ENTRYPOINT ["/app/saveany-bot"]
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
<!-- readme: contributors,AHCorn -start -->
|
<!-- readme: contributors -start -->
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tbody>
|
<tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- readme: contributors,AHCorn -end -->
|
<!-- readme: contributors -end -->
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto"
|
"github.com/celestix/gotgproto"
|
||||||
@@ -90,12 +89,10 @@ func Init() {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
common.Log.Fatal("初始化客户端失败: 超时")
|
common.Log.Panic("初始化客户端失败: 超时")
|
||||||
os.Exit(1)
|
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
common.Log.Fatalf("初始化客户端失败: %s", result.err)
|
common.Log.Panicf("初始化客户端失败: %s", result.err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
Client = result.client
|
Client = result.client
|
||||||
RegisterHandlers(Client.Dispatcher)
|
RegisterHandlers(Client.Dispatcher)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||||
// TODO: 回调数据用户独立鉴权 (处理 bot 在群聊中的情况)
|
|
||||||
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
|
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
@@ -116,8 +115,8 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
||||||
record.ReplyMessageID = update.CallbackQuery.MsgID
|
record.ReplyMessageID = update.CallbackQuery.MsgID
|
||||||
if err := dao.SaveReceivedFile(record); err != nil {
|
if _, err := dao.SaveReceivedFile(record); err != nil {
|
||||||
common.Log.Errorf("更新接收的文件失败: %s", err)
|
common.Log.Errorf("更新记录失败: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +168,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|||||||
task = types.Task{
|
task = types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
|
FileDBID: record.ID,
|
||||||
File: file,
|
File: file,
|
||||||
StorageName: storageName,
|
StorageName: storageName,
|
||||||
FileChatID: record.ChatID,
|
FileChatID: record.ChatID,
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
package bot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConversationType string
|
|
||||||
|
|
||||||
type ConversationState struct {
|
|
||||||
sync.Mutex
|
|
||||||
conversationType ConversationType
|
|
||||||
InConversation bool
|
|
||||||
data map[ConversationType]map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConversationState) Reset() {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
c.InConversation = false
|
|
||||||
c.conversationType = ""
|
|
||||||
c.data = make(map[ConversationType]map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConversationState) SetConversationType(t ConversationType) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
c.conversationType = t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConversationState) GetData(key string) interface{} {
|
|
||||||
if c.data == nil || c.data[c.conversationType] == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.data[c.conversationType][key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConversationState) SetData(key string, value interface{}) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
if c.data == nil {
|
|
||||||
c.data = make(map[ConversationType]map[string]interface{})
|
|
||||||
}
|
|
||||||
if c.data[c.conversationType] == nil {
|
|
||||||
c.data[c.conversationType] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
c.data[c.conversationType][key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement conversation handling
|
|
||||||
// var userConversationState = make(map[int64]*ConversationState)
|
|
||||||
|
|
||||||
// func handleConversation(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
// userID := update.EffectiveUser().GetID()
|
|
||||||
// state, ok := userConversationState[userID]
|
|
||||||
// if !ok {
|
|
||||||
// return dispatcher.ContinueGroups
|
|
||||||
// }
|
|
||||||
// if update.EffectiveMessage.Text == "/cancel" {
|
|
||||||
// state.Reset()
|
|
||||||
// ctx.Reply(update, ext.ReplyTextString("已取消"), nil)
|
|
||||||
// return dispatcher.EndGroups
|
|
||||||
// }
|
|
||||||
// if !state.InConversation {
|
|
||||||
// return dispatcher.ContinueGroups
|
|
||||||
// }
|
|
||||||
// return handleConversationState(ctx, update, state)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func handleConversationState(ctx *ext.Context, update *ext.Update, state *ConversationState) error {
|
|
||||||
// switch state.conversationType {
|
|
||||||
// default:
|
|
||||||
// common.Log.Errorf("Unknown conversation type: %s", state.conversationType)
|
|
||||||
// }
|
|
||||||
// return dispatcher.EndGroups
|
|
||||||
// }
|
|
||||||
@@ -77,7 +77,7 @@ func dirCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("路径ID无效"), nil)
|
ctx.Reply(update, ext.ReplyTextString("路径ID无效"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return delDir(ctx, update, user, dirID)
|
return delDir(ctx, update, dirID)
|
||||||
default:
|
default:
|
||||||
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
|
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -99,7 +99,7 @@ func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, p
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func delDir(ctx *ext.Context, update *ext.Update, user *dao.User, dirID int) error {
|
func delDir(ctx *ext.Context, update *ext.Update, dirID int) error {
|
||||||
if err := dao.DeleteDirByID(uint(dirID)); err != nil {
|
if err := dao.DeleteDirByID(uint(dirID)); err != nil {
|
||||||
common.Log.Errorf("删除路径失败: %s", err)
|
common.Log.Errorf("删除路径失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("删除路径失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString("删除路径失败"), nil)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"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)
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
storages := storage.GetUserStorages(user.ChatID)
|
// storages := storage.GetUserStorages(user.ChatID)
|
||||||
if len(storages) == 0 {
|
// if len(storages) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
return dispatcher.EndGroups
|
// return dispatcher.EndGroups
|
||||||
}
|
// }
|
||||||
|
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,14 +49,15 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
|
file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dao.SaveReceivedFile(&dao.ReceivedFile{
|
record, err := dao.SaveReceivedFile(&dao.ReceivedFile{
|
||||||
Processing: false,
|
Processing: false,
|
||||||
FileName: file.FileName,
|
FileName: file.FileName,
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
MessageID: update.EffectiveMessage.ID,
|
MessageID: update.EffectiveMessage.ID,
|
||||||
ReplyMessageID: msg.ID,
|
ReplyMessageID: msg.ID,
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
}); err != nil {
|
})
|
||||||
|
if err != nil {
|
||||||
common.Log.Errorf("添加接收的文件失败: %s", err)
|
common.Log.Errorf("添加接收的文件失败: %s", err)
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: fmt.Sprintf("添加接收的文件失败: %s", err),
|
Message: fmt.Sprintf("添加接收的文件失败: %s", err),
|
||||||
@@ -74,6 +74,7 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
|
FileDBID: record.ID,
|
||||||
File: file,
|
File: file,
|
||||||
StorageName: user.DefaultStorage,
|
StorageName: user.DefaultStorage,
|
||||||
FileChatID: update.EffectiveChat().GetID(),
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -10,7 +11,6 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,47 +19,47 @@ var (
|
|||||||
linkRegex = regexp.MustCompile(linkRegexString)
|
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 {
|
func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||||
common.Log.Trace("Got link message")
|
common.Log.Trace("Got link message")
|
||||||
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
||||||
if link == "" {
|
if link == "" {
|
||||||
return dispatcher.ContinueGroups
|
return dispatcher.ContinueGroups
|
||||||
}
|
}
|
||||||
strSlice := strings.Split(link, "/")
|
linkChatID, messageID, err := parseLink(ctx, link)
|
||||||
if len(strSlice) < 3 {
|
|
||||||
return dispatcher.ContinueGroups
|
|
||||||
}
|
|
||||||
messageID, err := strconv.Atoi(strSlice[len(strSlice)-1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Log.Errorf("解析消息 ID 失败: %s", err)
|
common.Log.Errorf("解析链接失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
ctx.Reply(update, ext.ReplyTextString("解析链接失败: "+err.Error()), 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)
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +69,13 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
storages := storage.GetUserStorages(user.ChatID)
|
|
||||||
|
|
||||||
if len(storages) == 0 {
|
// storages := storage.GetUserStorages(user.ChatID)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
// if len(storages) == 0 {
|
||||||
return dispatcher.EndGroups
|
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
}
|
// return dispatcher.EndGroups
|
||||||
|
// }
|
||||||
|
|
||||||
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Log.Errorf("回复失败: %s", err)
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
@@ -99,7 +100,8 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ReplyMessageID: replied.ID,
|
ReplyMessageID: replied.ID,
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
}
|
}
|
||||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
record, err := dao.SaveReceivedFile(receivedFile)
|
||||||
|
if err != nil {
|
||||||
common.Log.Errorf("保存接收的文件失败: %s", err)
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法保存文件: " + err.Error(),
|
Message: "无法保存文件: " + err.Error(),
|
||||||
@@ -113,6 +115,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
|
FileDBID: record.ID,
|
||||||
File: file,
|
File: file,
|
||||||
StorageName: user.DefaultStorage,
|
StorageName: user.DefaultStorage,
|
||||||
UserID: user.ChatID,
|
UserID: user.ChatID,
|
||||||
|
|||||||
@@ -63,12 +63,11 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
storages := storage.GetUserStorages(user.ChatID)
|
// storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
// if len(storages) == 0 {
|
||||||
if len(storages) == 0 {
|
// ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
// return dispatcher.EndGroups
|
||||||
return dispatcher.EndGroups
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
|
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,7 +113,8 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
record, err := dao.SaveReceivedFile(receivedFile)
|
||||||
|
if err != nil {
|
||||||
common.Log.Errorf("保存接收的文件失败: %s", err)
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: fmt.Sprintf("保存接收的文件失败: %s", err),
|
Message: fmt.Sprintf("保存接收的文件失败: %s", err),
|
||||||
@@ -130,6 +130,7 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
|
FileDBID: record.ID,
|
||||||
File: file,
|
File: file,
|
||||||
StorageName: user.DefaultStorage,
|
StorageName: user.DefaultStorage,
|
||||||
FileChatID: update.EffectiveChat().GetID(),
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
@@ -236,7 +237,8 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
ReplyMessageID: 0,
|
ReplyMessageID: 0,
|
||||||
}
|
}
|
||||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
record, err := dao.SaveReceivedFile(receivedFile)
|
||||||
|
if err != nil {
|
||||||
common.Log.Errorf("保存接收的文件失败: %s", err)
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
failedSaveDB++
|
failedSaveDB++
|
||||||
continue
|
continue
|
||||||
@@ -244,6 +246,7 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
task := &types.Task{
|
task := &types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
|
FileDBID: record.ID,
|
||||||
File: file,
|
File: file,
|
||||||
StorageName: user.DefaultStorage,
|
StorageName: user.DefaultStorage,
|
||||||
FileChatID: chatID,
|
FileChatID: chatID,
|
||||||
@@ -256,7 +259,7 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
successadd++
|
successadd++
|
||||||
}
|
}
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: fmt.Sprintf("批量保存完成\n成功添加: %d/%d\n获取文件失败: %d\n获取消息失败: %d\n保存数据库失败: %d", successadd, total, failedGetFile, failedGetMsg, failedSaveDB),
|
Message: fmt.Sprintf("批量添加任务完成\n成功添加: %d/%d\n获取文件失败: %d\n获取消息失败: %d\n保存数据库失败: %d", successadd, total, failedGetFile, failedGetMsg, failedSaveDB),
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ func handleTelegraph(ctx *ext.Context, update *ext.Update) error {
|
|||||||
IsTelegraph: true,
|
IsTelegraph: true,
|
||||||
TelegraphURL: tgphUrl,
|
TelegraphURL: tgphUrl,
|
||||||
}
|
}
|
||||||
if err := dao.SaveReceivedFile(record); err != nil {
|
if _, err := dao.SaveReceivedFile(record); err != nil {
|
||||||
common.Log.Errorf("保存接收的文件失败: %s", err)
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法保存文件: " + err.Error(),
|
Message: "无法保存文件: " + err.Error(),
|
||||||
|
|||||||
@@ -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("add"), AddToQueue))
|
||||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage))
|
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("cancel"), cancelTask))
|
||||||
|
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("send_here"), sendFileToTelegram))
|
||||||
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/gotd/contrib/middleware/floodwait"
|
"github.com/gotd/contrib/middleware/floodwait"
|
||||||
"github.com/gotd/contrib/middleware/ratelimit"
|
"github.com/gotd/contrib/middleware/ratelimit"
|
||||||
"github.com/gotd/td/telegram"
|
"github.com/gotd/td/telegram"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
@@ -30,8 +31,44 @@ const noPermissionText string = `
|
|||||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
||||||
|
if config.Cfg.AsPublicCopyMediaBot {
|
||||||
|
tryCopyMedia(ctx, update)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return dispatcher.ContinueGroups
|
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
|
||||||
|
}
|
||||||
|
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 _, ok := msg.GetMedia(); !ok || msg.Media == nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("消息中没有文件或媒体"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Log.Tracef("Got copy media request from %d", update.EffectiveChat().GetID())
|
||||||
|
if _, err := copyMediaToChat(ctx, msg, update.EffectiveChat().GetID()); err != nil {
|
||||||
|
common.Log.Errorf("Failed to copy media: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
bot/utils.go
19
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)
|
return nil, fmt.Errorf("failed to get user by chat ID: %d, error: %w", userChatID, err)
|
||||||
}
|
}
|
||||||
storages := storage.GetUserStorages(user.ChatID)
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
if len(storages) == 0 {
|
// if len(storages) == 0 {
|
||||||
return nil, ErrNoStorages
|
// return nil, ErrNoStorages
|
||||||
}
|
// }
|
||||||
|
|
||||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
for _, storage := range storages {
|
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))]
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
markup.Rows = append(markup.Rows, row)
|
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
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +190,9 @@ func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileNa
|
|||||||
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
||||||
cachedFile, err := common.CacheGet[*types.File](ctx, key)
|
cachedFile, err := common.CacheGet[*types.File](ctx, key)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if customFileName != "" {
|
||||||
|
cachedFile.FileName = customFileName
|
||||||
|
}
|
||||||
return cachedFile, nil
|
return cachedFile, nil
|
||||||
}
|
}
|
||||||
common.Log.Debugf("Getting file: %s", key)
|
common.Log.Debugf("Getting file: %s", key)
|
||||||
@@ -205,7 +216,7 @@ func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, e
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return cacheMessage, 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}})
|
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func Run(_ *cobra.Command, _ []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
common.Log.Info("正在清理缓存文件夹: ", cachePath)
|
common.Log.Info("正在清理缓存文件夹: ", cachePath)
|
||||||
if err := os.RemoveAll(cachePath); err != nil {
|
if err := common.RemoveAllInDir(cachePath); err != nil {
|
||||||
common.Log.Error("清理缓存失败: ", err)
|
common.Log.Error("清理缓存失败: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,16 @@ func InitLogger() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
consoleH := handler.NewConsoleHandler(logLevels)
|
consoleH := handler.NewConsoleHandler(logLevels)
|
||||||
fileH, err := handler.NewTimeRotateFile(
|
Log.AddHandler(consoleH)
|
||||||
logFilePath,
|
if logFilePath != "" && logBackupNum > 0 {
|
||||||
rotatefile.EveryDay,
|
fileH, err := handler.NewTimeRotateFile(
|
||||||
handler.WithLogLevels(slog.AllLevels),
|
logFilePath,
|
||||||
handler.WithBackupNum(logBackupNum),
|
rotatefile.EveryDay,
|
||||||
handler.WithBuffSize(0),
|
handler.WithLogLevels(slog.AllLevels),
|
||||||
)
|
handler.WithBackupNum(logBackupNum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
}
|
||||||
|
Log.AddHandler(fileH)
|
||||||
}
|
}
|
||||||
Log.AddHandlers(consoleH, fileH)
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
common/os.go
35
common/os.go
@@ -1,31 +1,11 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建文件, 自动创建目录
|
|
||||||
func MkFile(path string, data []byte) error {
|
|
||||||
err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(path, data, os.ModePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文件, 并清理空目录. 如果文件不存在则返回 nil
|
|
||||||
func PurgeFile(path string) error {
|
|
||||||
if err := os.Remove(path); err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RemoveEmptyDirectories(filepath.Dir(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
func RmFileAfter(path string, td time.Duration) {
|
func RmFileAfter(path string, td time.Duration) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,22 +14,23 @@ func RmFileAfter(path string, td time.Duration) {
|
|||||||
}
|
}
|
||||||
Log.Debugf("Remove file after %s: %s", td, path)
|
Log.Debugf("Remove file after %s: %s", td, path)
|
||||||
time.AfterFunc(td, func() {
|
time.AfterFunc(td, func() {
|
||||||
PurgeFile(path)
|
if err := os.Remove(path); err != nil {
|
||||||
|
Log.Errorf("Failed to remove file %s: %s", path, err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归删除空目录
|
// 删除目录下的所有内容, 但不删除目录本身
|
||||||
func RemoveEmptyDirectories(dirPath string) error {
|
func RemoveAllInDir(dirPath string) error {
|
||||||
entries, err := os.ReadDir(dirPath)
|
entries, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
for _, entry := range entries {
|
||||||
err := os.Remove(dirPath)
|
entryPath := filepath.Join(dirPath, entry.Name())
|
||||||
if err != nil {
|
if err := os.RemoveAll(entryPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return RemoveEmptyDirectories(filepath.Dir(dirPath))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ type Config struct {
|
|||||||
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
||||||
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
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"`
|
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||||
|
|
||||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||||
@@ -90,11 +93,9 @@ func Init() error {
|
|||||||
viper.SetDefault("telegram.rpc_retry", 5)
|
viper.SetDefault("telegram.rpc_retry", 5)
|
||||||
|
|
||||||
viper.SetDefault("temp.base_path", "cache/")
|
viper.SetDefault("temp.base_path", "cache/")
|
||||||
viper.SetDefault("temp.cache_ttl", 3600)
|
viper.SetDefault("temp.cache_ttl", 30)
|
||||||
|
|
||||||
viper.SetDefault("log.level", "INFO")
|
viper.SetDefault("log.level", "INFO")
|
||||||
viper.SetDefault("log.file", "logs/saveany.log")
|
|
||||||
viper.SetDefault("log.backup_count", 7)
|
|
||||||
|
|
||||||
viper.SetDefault("db.path", "data/saveany.db")
|
viper.SetDefault("db.path", "data/saveany.db")
|
||||||
viper.SetDefault("db.session", "data/session.db")
|
viper.SetDefault("db.session", "data/session.db")
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ import (
|
|||||||
|
|
||||||
func processPendingTask(task *types.Task) error {
|
func processPendingTask(task *types.Task) error {
|
||||||
common.Log.Debugf("Start processing task: %s", task.String())
|
common.Log.Debugf("Start processing task: %s", task.String())
|
||||||
|
|
||||||
|
if task.FileName() != "" && !task.IsTelegraph && task.File.FileSize != 0 && task.FileDBID != 0 {
|
||||||
|
ext := path.Ext(task.FileName())
|
||||||
|
name := task.FileName()[:len(task.FileName())-len(ext)]
|
||||||
|
task.File.FileName = fmt.Sprintf("%s_%d%s", name, task.FileDBID, ext)
|
||||||
|
}
|
||||||
|
|
||||||
if task.FileName() == "" {
|
if task.FileName() == "" {
|
||||||
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
||||||
}
|
}
|
||||||
@@ -60,6 +67,7 @@ func processPendingTask(task *types.Task) error {
|
|||||||
|
|
||||||
notsupportStreamStorage, notsupportStream := taskStorage.(storage.StorageNotSupportStream)
|
notsupportStreamStorage, notsupportStream := taskStorage.(storage.StorageNotSupportStream)
|
||||||
cancelMarkUp := getCancelTaskMarkup(task)
|
cancelMarkUp := getCancelTaskMarkup(task)
|
||||||
|
|
||||||
if config.Cfg.Stream {
|
if config.Cfg.Stream {
|
||||||
if !notsupportStream {
|
if !notsupportStream {
|
||||||
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
func SaveReceivedFile(receivedFile *ReceivedFile) error {
|
func SaveReceivedFile(receivedFile *ReceivedFile) (*ReceivedFile, error) {
|
||||||
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
receivedFile.ID = record.ID
|
receivedFile.ID = record.ID
|
||||||
}
|
}
|
||||||
return db.Save(receivedFile).Error
|
db.Save(receivedFile)
|
||||||
|
return receivedFile, db.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*ReceivedFile, error) {
|
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*ReceivedFile, error) {
|
||||||
|
|||||||
13
docker-compose.local.yml
Normal file
13
docker-compose.local.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
saveany-bot:
|
||||||
|
build: .
|
||||||
|
container_name: saveany-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./config.toml:/app/config.toml
|
||||||
|
- ./downloads:/app/downloads
|
||||||
|
- ./cache:/app/cache
|
||||||
|
# 使用 host 模式以便访问宿主机服务 (如代理)
|
||||||
|
# 如果你对 Docker 网络模式熟悉, 可以自行修改
|
||||||
|
network_mode: host
|
||||||
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
|
||||||
|
|
||||||
|
同上, 根据消息文本内容正则匹配
|
||||||
|
|
||||||
|
## 复制并发送媒体消息
|
||||||
|
|
||||||
|
将接收到的文件(媒体)消息, 或链接对应的消息原样发送到当前聊天, 点击选择存储按钮中的 "发送到当前聊天" 即可.
|
||||||
@@ -30,5 +30,6 @@ nav:
|
|||||||
- index.md
|
- index.md
|
||||||
- deploy.md
|
- deploy.md
|
||||||
- help.md
|
- help.md
|
||||||
|
- experimental.md
|
||||||
- faq.md
|
- faq.md
|
||||||
- contribute.md
|
- contribute.md
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
@@ -113,8 +115,13 @@ func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
|
func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
|
||||||
url := c.BaseURL + remotePath
|
u, err := url.Parse(c.BaseURL)
|
||||||
resp, err := c.doRequest(ctx, WebdavMethodPut, url, content)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.Trim(remotePath, "/"), "/")
|
||||||
|
u.Path = path.Join(u.Path, strings.Join(parts, "/"))
|
||||||
|
resp, err := c.doRequest(ctx, WebdavMethodPut, u.String(), content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -124,4 +131,5 @@ func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Re
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("PUT: %s", resp.Status)
|
return fmt.Errorf("PUT: %s", resp.Status)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Task struct {
|
|||||||
StorageName string
|
StorageName string
|
||||||
StoragePath string
|
StoragePath string
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
|
FileDBID uint
|
||||||
|
|
||||||
File *File
|
File *File
|
||||||
FileMessageID int
|
FileMessageID int
|
||||||
|
|||||||
Reference in New Issue
Block a user