Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87e8836c78 | ||
|
|
1a7747c2d2 | ||
|
|
ca0fd67fba | ||
|
|
4d736b925b | ||
|
|
ead2b20f4e | ||
|
|
080d474714 | ||
|
|
f453205fde | ||
|
|
407677f270 | ||
|
|
958bfd1dbe | ||
|
|
debe33d84d | ||
|
|
52eead3bf5 | ||
|
|
0af049a507 | ||
|
|
8752dd865c | ||
|
|
c0b4580e34 | ||
|
|
280fd6ead8 | ||
|
|
0ca3d97711 | ||
|
|
51198a1e3d | ||
|
|
651835c467 | ||
|
|
45c978980c | ||
|
|
c21ff7e499 | ||
|
|
32cc1e4b5a | ||
|
|
c974791dc0 | ||
|
|
91814a83c7 | ||
|
|
685047e463 | ||
|
|
37e9c79ceb | ||
|
|
494d1bf51c | ||
|
|
a6f194aedd | ||
|
|
acd16a91a3 | ||
|
|
75f79e8abc | ||
|
|
1065acfdb8 | ||
|
|
fef7d37a7e | ||
|
|
b5e9cf987a | ||
|
|
c58fa454bb | ||
|
|
2c5d6f0e57 | ||
|
|
7d57ad30a9 | ||
|
|
4f314bd37f | ||
|
|
131dfeb4cd | ||
|
|
3f40acff55 | ||
|
|
fe47ee3b51 | ||
|
|
4a6f63e58f | ||
|
|
16c71e6384 | ||
|
|
0c2d116708 | ||
|
|
450d32b2b7 | ||
|
|
f80ecae3cc | ||
|
|
f0853536d9 | ||
|
|
15cf81e1bd | ||
|
|
ae48bd52bf | ||
|
|
44de871f63 | ||
|
|
7a2a530e49 |
@@ -9,3 +9,4 @@ cache/
|
|||||||
docs/
|
docs/
|
||||||
config.example.toml
|
config.example.toml
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
|
playwright/
|
||||||
143
.github/workflows/build-docker.yml
vendored
143
.github/workflows/build-docker.yml
vendored
@@ -7,15 +7,26 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: krau/saveany-bot
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docker-build-${{ github.repository }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
prepare:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
version: ${{ steps.meta.outputs.version }}
|
||||||
|
git_commit: ${{ steps.args.outputs.git_commit }}
|
||||||
|
build_time: ${{ steps.args.outputs.build_time }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -29,7 +40,24 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=sha
|
type=sha
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- name: Extract Dockerfile args
|
||||||
|
id: args
|
||||||
|
run: |
|
||||||
|
echo "git_commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_time=$(git show -s --format=%cI)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-default:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -44,26 +72,113 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract Dockerfile args
|
|
||||||
id: args
|
|
||||||
run: |
|
|
||||||
echo "git_commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "build_time=$(git show -s --format=%cI)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ needs.prepare.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ needs.prepare.outputs.labels }}
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
type=gha
|
type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ steps.meta.outputs.version }}
|
VERSION=${{ needs.prepare.outputs.version }}
|
||||||
GitCommit=${{ steps.args.outputs.git_commit }}
|
GitCommit=${{ needs.prepare.outputs.git_commit }}
|
||||||
BuildTime=${{ steps.args.outputs.build_time }}
|
BuildTime=${{ needs.prepare.outputs.build_time }}
|
||||||
|
|
||||||
|
build-micro:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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: Build and push micro Docker image
|
||||||
|
id: build-and-push-micro
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.micro
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:micro
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:micro-latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:micro-${{ needs.prepare.outputs.version }}
|
||||||
|
labels: ${{ needs.prepare.outputs.labels }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:micro-latest
|
||||||
|
type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ needs.prepare.outputs.version }}
|
||||||
|
GitCommit=${{ needs.prepare.outputs.git_commit }}
|
||||||
|
BuildTime=${{ needs.prepare.outputs.build_time }}
|
||||||
|
|
||||||
|
build-pico:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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: Build and push pico Docker image
|
||||||
|
id: build-and-push-pico
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.pico
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pico
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pico-latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pico-${{ needs.prepare.outputs.version }}
|
||||||
|
labels: ${{ needs.prepare.outputs.labels }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pico-latest
|
||||||
|
type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ needs.prepare.outputs.version }}
|
||||||
|
GitCommit=${{ needs.prepare.outputs.git_commit }}
|
||||||
|
BuildTime=${{ needs.prepare.outputs.build_time }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ cache.db
|
|||||||
.vscode/
|
.vscode/
|
||||||
temp/
|
temp/
|
||||||
.hugo_build.lock
|
.hugo_build.lock
|
||||||
|
playwright/
|
||||||
|
testplugins/
|
||||||
@@ -20,12 +20,13 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.GitCommit=${GitCommit}' \
|
-X 'github.com/krau/SaveAny-Bot/config.GitCommit=${GitCommit}' \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.BuildTime=${BuildTime}' \
|
-X 'github.com/krau/SaveAny-Bot/config.BuildTime=${BuildTime}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.Docker=true' \
|
||||||
" \
|
" \
|
||||||
-o saveany-bot .
|
-o saveany-bot .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl ffmpeg
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
41
Dockerfile.micro
Normal file
41
Dockerfile.micro
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG GitCommit="Unknown"
|
||||||
|
ARG BuildTime="Unknown"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg \
|
||||||
|
CGO_ENABLED=0 \
|
||||||
|
go build -trimpath \
|
||||||
|
-tags=no_jsparser,no_minio \
|
||||||
|
-ldflags=" \
|
||||||
|
-s -w \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.GitCommit=${GitCommit}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.BuildTime=${BuildTime}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.Docker=true' \
|
||||||
|
" \
|
||||||
|
-o saveany-bot .
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/saveany-bot .
|
||||||
|
COPY entrypoint.sh .
|
||||||
|
|
||||||
|
RUN chmod +x /app/saveany-bot && \
|
||||||
|
chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|
||||||
36
Dockerfile.pico
Normal file
36
Dockerfile.pico
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# pico is the minimum build of SaveAnyBot, which disables all the optional features like JS parsing and MinIO support.
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG GitCommit="Unknown"
|
||||||
|
ARG BuildTime="Unknown"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg \
|
||||||
|
CGO_ENABLED=0 \
|
||||||
|
go build -trimpath \
|
||||||
|
-tags=no_jsparser,no_minio,sqlite_glebarez \
|
||||||
|
-ldflags=" \
|
||||||
|
-s -w \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.GitCommit=${GitCommit}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.BuildTime=${BuildTime}' \
|
||||||
|
-X 'github.com/krau/SaveAny-Bot/config.Docker=true' \
|
||||||
|
" \
|
||||||
|
-o saveany-bot .
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/saveany-bot .
|
||||||
|
RUN chmod +x /app/saveany-bot
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/saveany-bot"]
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
- 使用 js 编写解析器插件以转存任意网站的文件
|
- 使用 js 编写解析器插件以转存任意网站的文件
|
||||||
- 存储端支持:
|
- 存储端支持:
|
||||||
- Alist
|
- Alist
|
||||||
- S3 (MinioSDK)
|
- S3
|
||||||
- WebDAV
|
- WebDAV
|
||||||
- 本地磁盘
|
- 本地磁盘
|
||||||
- Telegram (重传回指定聊天)
|
- Telegram (重传回指定聊天)
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/celestix/gotgproto/sessionMaker"
|
"github.com/celestix/gotgproto/sessionMaker"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/telegram/dcs"
|
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers"
|
||||||
"github.com/krau/SaveAny-Bot/client/middleware"
|
"github.com/krau/SaveAny-Bot/client/middleware"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/ncruces/go-sqlite3/gormlite"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(ctx context.Context) <-chan struct{} {
|
func Init(ctx context.Context) <-chan struct{} {
|
||||||
@@ -26,29 +24,22 @@ func Init(ctx context.Context) <-chan struct{} {
|
|||||||
err error
|
err error
|
||||||
})
|
})
|
||||||
shouldRestart := make(chan struct{})
|
shouldRestart := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var resolver dcs.Resolver
|
resolver, err := tgutil.NewConfigProxyResolver()
|
||||||
if config.C().Telegram.Proxy.Enable && config.C().Telegram.Proxy.URL != "" {
|
if err != nil {
|
||||||
dialer, err := netutil.NewProxyDialer(config.C().Telegram.Proxy.URL)
|
resultChan <- struct {
|
||||||
if err != nil {
|
client *gotgproto.Client
|
||||||
resultChan <- struct {
|
err error
|
||||||
client *gotgproto.Client
|
}{nil, err}
|
||||||
err error
|
return
|
||||||
}{nil, err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolver = dcs.Plain(dcs.PlainOptions{
|
|
||||||
Dial: dialer.(proxy.ContextDialer).DialContext,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolver = dcs.DefaultResolver()
|
|
||||||
}
|
}
|
||||||
client, err := gotgproto.NewClient(
|
client, err := gotgproto.NewClient(
|
||||||
config.C().Telegram.AppID,
|
config.C().Telegram.AppID,
|
||||||
config.C().Telegram.AppHash,
|
config.C().Telegram.AppHash,
|
||||||
gotgproto.ClientTypeBot(config.C().Telegram.Token),
|
gotgproto.ClientTypeBot(config.C().Telegram.Token),
|
||||||
&gotgproto.ClientOpts{
|
&gotgproto.ClientOpts{
|
||||||
Session: sessionMaker.SqlSession(gormlite.Open(config.C().DB.Session)),
|
Session: sessionMaker.SqlSession(database.GetDialect(config.C().DB.Session)),
|
||||||
DisableCopyright: true,
|
DisableCopyright: true,
|
||||||
Middlewares: middleware.NewDefaultMiddlewares(ctx, 5*time.Minute),
|
Middlewares: middleware.NewDefaultMiddlewares(ctx, 5*time.Minute),
|
||||||
Resolver: resolver,
|
Resolver: resolver,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
|
|
||||||
if !data.SettedDir && len(dirs) != 0 {
|
if !data.SettedDir && len(dirs) != 0 {
|
||||||
// ask for directory selection
|
// ask for directory selection
|
||||||
markup, err := msgelem.BuildSetDirKeyboard(dirs, dataid)
|
markup, err := msgelem.BuildSetDirMarkupForAdd(dirs, dataid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "目录键盘构建失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "目录键盘构建失败: "+err.Error()))
|
||||||
@@ -80,8 +80,10 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
dirPath = path.Join(dirPath, fsutil.NormalizePathname(data.ParsedItem.Title))
|
dirPath = path.Join(dirPath, fsutil.NormalizePathname(data.ParsedItem.Title))
|
||||||
}
|
}
|
||||||
shortcut.CreateAndAddParsedTaskWithEdit(ctx, selectedStorage, dirPath, data.ParsedItem, msgID, userID)
|
shortcut.CreateAndAddParsedTaskWithEdit(ctx, selectedStorage, dirPath, data.ParsedItem, msgID, userID)
|
||||||
|
case tasktype.TaskTypeDirectlinks:
|
||||||
|
shortcut.CreateAndAddDirectTaskWithEdit(ctx, selectedStorage, dirPath, data.DirectLinks, msgID, userID)
|
||||||
default:
|
default:
|
||||||
log.FromContext(ctx).Errorf("Unsupported task type: %s", data.TaskType)
|
return fmt.Errorf("unexcept task type: %s", data.TaskType)
|
||||||
}
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,20 @@ func handleCancelCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleCancelCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
args := strings.Fields(update.EffectiveMessage.Text)
|
||||||
|
if len(args) < 2 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("用法: /cancel <task_id>"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
taskID := args[1]
|
||||||
|
if err := core.CancelTask(ctx, taskID); err != nil {
|
||||||
|
logger.Errorf("failed to cancel task %s: %v", taskID, err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("取消任务失败: "+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("已请求取消任务: "+taskID), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|||||||
49
client/bot/handlers/dl.go
Normal file
49
client/bot/handlers/dl.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleDlCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
|
if len(args) < 2 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("用法: /dl <链接1> <链接2> ..."), nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
links := args[1:]
|
||||||
|
for i, link := range links {
|
||||||
|
links[i] = strings.TrimSpace(link)
|
||||||
|
u, err := url.Parse(link)
|
||||||
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
|
logger.Warn("invaild link", link)
|
||||||
|
links[i] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
links = slice.Compact(links)
|
||||||
|
if len(links) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("没有有效的链接可供下载"), nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
markup, err := msgelem.BuildAddSelectStorageKeyboard(storage.GetUserStorages(ctx, update.GetUserChat().GetID()), tcbdata.Add{
|
||||||
|
TaskType: tasktype.TaskTypeDirectlinks,
|
||||||
|
DirectLinks: links,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(links))), &ext.ReplyOpts{
|
||||||
|
Markup: markup,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
@@ -43,20 +44,14 @@ func handleMessageLink(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSilentSaveLink(ctx *ext.Context, update *ext.Update) error {
|
func handleSilentSaveLink(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
if stor == nil {
|
|
||||||
logger.Warn("Context storage is nil")
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("未找到存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
replied, files, _, err := shortcut.GetFilesFromUpdateLinkMessageWithReplyEdit(ctx, update)
|
replied, files, _, err := shortcut.GetFilesFromUpdateLinkMessageWithReplyEdit(ctx, update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userId := update.GetUserChat().GetID()
|
userId := update.GetUserChat().GetID()
|
||||||
if len(files) == 1 {
|
if len(files) == 1 {
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", files[0], replied.ID)
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, dirutil.PathFromContext(ctx), files[0], replied.ID)
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", files, replied.ID)
|
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, dirutil.PathFromContext(ctx), files, replied.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,12 +25,6 @@ func handleMediaMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tfOpts := make([]tfile.TGFileOption, 0)
|
|
||||||
// switch userDB.FilenameStrategy {
|
|
||||||
// case fnamest.Message.String():
|
|
||||||
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,11 +45,6 @@ func handleMediaMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
func handleSilentSaveMedia(ctx *ext.Context, update *ext.Update) error {
|
func handleSilentSaveMedia(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
if stor == nil {
|
|
||||||
logger.Warn("Context storage is nil")
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("未找到存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
message := update.EffectiveMessage.Message
|
message := update.EffectiveMessage.Message
|
||||||
groupID, isGroup := message.GetGroupedID()
|
groupID, isGroup := message.GetGroupedID()
|
||||||
if isGroup && groupID != 0 {
|
if isGroup && groupID != 0 {
|
||||||
@@ -74,109 +56,10 @@ func handleSilentSaveMedia(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tfOpts := make([]tfile.TGFileOption, 0)
|
|
||||||
// switch userDB.FilenameStrategy {
|
|
||||||
// case fnamest.Message.String():
|
|
||||||
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, stor, "", file, msg.ID)
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, stor, dirutil.PathFromContext(ctx), file, msg.ID)
|
||||||
}
|
|
||||||
|
|
||||||
type MediaGroupHandler struct {
|
|
||||||
groups map[int64][]tfile.TGFileMessage
|
|
||||||
timers map[int64]*time.Timer
|
|
||||||
mu sync.Mutex
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
var mediaGroupHandler = &MediaGroupHandler{
|
|
||||||
groups: make(map[int64][]tfile.TGFileMessage),
|
|
||||||
timers: make(map[int64]*time.Timer),
|
|
||||||
timeout: 1 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGroupMediaMessage(ctx *ext.Context, update *ext.Update, message *tg.Message, groupID int64) error {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
media := message.Media
|
|
||||||
supported := mediautil.IsSupported(media)
|
|
||||||
if !supported {
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfile.WithNameIfEmpty(
|
|
||||||
tgutil.GenFileNameFromMessage(*message),
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get file from media: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
mediaGroupHandler.mu.Lock()
|
|
||||||
defer mediaGroupHandler.mu.Unlock()
|
|
||||||
if mediaGroupHandler.groups[groupID] == nil {
|
|
||||||
mediaGroupHandler.groups[groupID] = make([]tfile.TGFileMessage, 0)
|
|
||||||
}
|
|
||||||
mediaGroupHandler.groups[groupID] = append(mediaGroupHandler.groups[groupID], file)
|
|
||||||
|
|
||||||
if timer, exists := mediaGroupHandler.timers[groupID]; exists {
|
|
||||||
timer.Stop()
|
|
||||||
}
|
|
||||||
mediaGroupHandler.timers[groupID] = time.AfterFunc(mediaGroupHandler.timeout, func() {
|
|
||||||
processMediaGroup(ctx, update, groupID)
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
mediaGroupHandler.mu.Lock()
|
|
||||||
items := mediaGroupHandler.groups[groupID]
|
|
||||||
delete(mediaGroupHandler.groups, groupID)
|
|
||||||
delete(mediaGroupHandler.timers, groupID)
|
|
||||||
mediaGroupHandler.mu.Unlock()
|
|
||||||
if len(items) == 0 {
|
|
||||||
logger.Warn("No media items to process for group", "groupID", groupID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
|
||||||
|
|
||||||
userId := update.GetUserChat().GetID()
|
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在保存文件..."), nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to reply: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stor := storage.FromContext(ctx)
|
|
||||||
if stor != nil {
|
|
||||||
// In silent mode
|
|
||||||
if len(items) == 1 {
|
|
||||||
shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", items[0], msg.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", items, msg.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stors := storage.GetUserStorages(ctx, userId)
|
|
||||||
markup, err := msgelem.BuildAddSelectStorageKeyboard(stors, tcbdata.Add{
|
|
||||||
Files: items,
|
|
||||||
AsBatch: len(items) > 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("构建存储选择键盘失败: %s", err)
|
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
|
||||||
ID: msg.ID,
|
|
||||||
Message: "构建存储选择键盘失败: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
|
||||||
ID: msg.ID,
|
|
||||||
Message: fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(items)),
|
|
||||||
ReplyMarkup: markup,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
126
client/bot/handlers/media_group.go
Normal file
126
client/bot/handlers/media_group.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaGroupHandler struct {
|
||||||
|
groups map[int64][]tfile.TGFileMessage
|
||||||
|
timers map[int64]*time.Timer
|
||||||
|
mu sync.Mutex
|
||||||
|
timeout time.Duration
|
||||||
|
setupOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaGroupHandler) SetupTimeout(timeoutSec int) {
|
||||||
|
m.setupOnce.Do(func() {
|
||||||
|
if timeoutSec < 1 {
|
||||||
|
timeoutSec = 1
|
||||||
|
}
|
||||||
|
m.timeout = time.Duration(timeoutSec) * time.Second
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mediaGroupHandler = &MediaGroupHandler{
|
||||||
|
groups: make(map[int64][]tfile.TGFileMessage),
|
||||||
|
timers: make(map[int64]*time.Timer),
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleGroupMediaMessage(ctx *ext.Context, update *ext.Update, message *tg.Message, groupID int64) error {
|
||||||
|
mediaGroupHandler.SetupTimeout(max(config.C().Telegram.MediaGroupTimeout, 1))
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
media := message.Media
|
||||||
|
supported := mediautil.IsSupported(media)
|
||||||
|
if !supported {
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfile.WithNameIfEmpty(
|
||||||
|
tgutil.GenFileNameFromMessage(*message),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get file from media: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
mediaGroupHandler.mu.Lock()
|
||||||
|
defer mediaGroupHandler.mu.Unlock()
|
||||||
|
if mediaGroupHandler.groups[groupID] == nil {
|
||||||
|
mediaGroupHandler.groups[groupID] = make([]tfile.TGFileMessage, 0)
|
||||||
|
}
|
||||||
|
mediaGroupHandler.groups[groupID] = append(mediaGroupHandler.groups[groupID], file)
|
||||||
|
|
||||||
|
if timer, exists := mediaGroupHandler.timers[groupID]; exists {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
mediaGroupHandler.timers[groupID] = time.AfterFunc(mediaGroupHandler.timeout, func() {
|
||||||
|
processMediaGroup(ctx, update, groupID)
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
mediaGroupHandler.mu.Lock()
|
||||||
|
items := mediaGroupHandler.groups[groupID]
|
||||||
|
delete(mediaGroupHandler.groups, groupID)
|
||||||
|
delete(mediaGroupHandler.timers, groupID)
|
||||||
|
mediaGroupHandler.mu.Unlock()
|
||||||
|
if len(items) == 0 {
|
||||||
|
logger.Warn("No media items to process for group", "groupID", groupID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
||||||
|
|
||||||
|
userId := update.GetUserChat().GetID()
|
||||||
|
msg, err := ctx.Reply(update, ext.ReplyTextString("正在保存文件..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to reply: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stor := storage.FromContext(ctx)
|
||||||
|
if stor != nil {
|
||||||
|
// In silent mode
|
||||||
|
if len(items) == 1 {
|
||||||
|
shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", items[0], msg.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", items, msg.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stors := storage.GetUserStorages(ctx, userId)
|
||||||
|
markup, err := msgelem.BuildAddSelectStorageKeyboard(stors, tcbdata.Add{
|
||||||
|
Files: items,
|
||||||
|
AsBatch: len(items) > 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("构建存储选择键盘失败: %s", err)
|
||||||
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: msg.ID,
|
||||||
|
Message: "构建存储选择键盘失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: msg.ID,
|
||||||
|
Message: fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(items)),
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -43,6 +44,14 @@ func handleSilentMode(next func(*ext.Context, *ext.Update) error, handler func(*
|
|||||||
ctx.Reply(update, ext.ReplyTextString("获取默认存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取默认存储失败: "+err.Error()), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
if user.DefaultDir != 0 {
|
||||||
|
dir, err := database.GetDirByID(ctx, user.DefaultDir)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取默认文件夹失败: "+err.Error()), nil)
|
||||||
|
return next(ctx, update)
|
||||||
|
}
|
||||||
|
ctx.Context = dirutil.WithContext(ctx.Context, dir)
|
||||||
|
}
|
||||||
ctx.Context = storage.WithContext(ctx.Context, stor)
|
ctx.Context = storage.WithContext(ctx.Context, stor)
|
||||||
return handler(ctx, update)
|
return handler(ctx, update)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
@@ -75,11 +77,6 @@ func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
|||||||
func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
if stor == nil {
|
|
||||||
logger.Warn("Context storage is nil")
|
|
||||||
ctx.Reply(u, ext.ReplyTextString("未找到存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
text := u.EffectiveMessage.Text
|
text := u.EffectiveMessage.Text
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -117,5 +114,8 @@ func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
|||||||
if len(item.Resources) > 1 {
|
if len(item.Resources) > 1 {
|
||||||
dirPath = fsutil.NormalizePathname(item.Title)
|
dirPath = fsutil.NormalizePathname(item.Title)
|
||||||
}
|
}
|
||||||
|
if p := dirutil.PathFromContext(ctx); p != "" {
|
||||||
|
dirPath = path.Join(p, dirPath)
|
||||||
|
}
|
||||||
return shortcut.CreateAndAddParsedTaskWithEdit(ctx, stor, dirPath, item, msg.ID, userID)
|
return shortcut.CreateAndAddParsedTaskWithEdit(ctx, stor, dirPath, item, msg.ID, userID)
|
||||||
}
|
}
|
||||||
|
|||||||
97
client/bot/handlers/parser.go
Normal file
97
client/bot/handlers/parser.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleParserCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
|
args := strings.Split(u.EffectiveMessage.Text, " ")
|
||||||
|
help := `
|
||||||
|
用法:
|
||||||
|
|
||||||
|
/parser install <回复一个文件> - 安装解析器
|
||||||
|
`
|
||||||
|
if len(args) < 2 {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString(help), nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch args[1] {
|
||||||
|
// case "list":
|
||||||
|
// return handleParserListCmd(ctx, u)
|
||||||
|
case "install":
|
||||||
|
return handleParserInstallCmd(ctx, u)
|
||||||
|
// case "uninstall":
|
||||||
|
// return handleParserUninstallCmd(ctx, u)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleParserInstallCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
|
if !config.C().Parser.PluginEnable {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("解析器插件功能未启用"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if u.EffectiveMessage.ReplyToMessage == nil || u.EffectiveMessage.ReplyToMessage.Media == nil {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("请回复一个包含解析器文件的消息"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
media := u.EffectiveMessage.ReplyToMessage.Media
|
||||||
|
document, ok := media.(*tg.MessageMediaDocument)
|
||||||
|
if !ok {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
value, ok := document.GetDocument()
|
||||||
|
if !ok {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
doc, ok := value.AsNotEmpty()
|
||||||
|
if !ok {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(doc.MimeType, "text/") {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("错误的文件类型"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if doc.Size > 1024*1024*10 {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("文件过大"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
var fileName string
|
||||||
|
for _, attr := range doc.Attributes {
|
||||||
|
if fileNameAttr, ok := attr.(*tg.DocumentAttributeFilename); ok {
|
||||||
|
fileName = fileNameAttr.FileName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("无法获取文件名"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(fileName, ".js") {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("仅支持 .js 文件作为解析器"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
data := bytes.NewBuffer(nil)
|
||||||
|
_, err := ctx.DownloadMedia(media, ext.DownloadOutputStream{Writer: data}, nil)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("文件下载失败: "+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if err := parsers.AddPlugin(ctx, data.String(), fileName); err != nil {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("插件安装失败: "+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(u, ext.ReplyTextString("插件安装成功: "+fileName), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
@@ -26,14 +26,18 @@ var CommandHandlers = []DescCommandHandler{
|
|||||||
{"storage", "设置默认存储端", handleStorageCmd},
|
{"storage", "设置默认存储端", handleStorageCmd},
|
||||||
{"dir", "管理存储文件夹", handleDirCmd},
|
{"dir", "管理存储文件夹", handleDirCmd},
|
||||||
{"rule", "管理自动存储规则", handleRuleCmd},
|
{"rule", "管理自动存储规则", handleRuleCmd},
|
||||||
|
{"save", "保存文件", handleSilentMode(handleSaveCmd, handleSilentSaveReplied)},
|
||||||
|
{"dl", "下载给定链接的文件", handleDlCmd},
|
||||||
|
{"task", "管理任务队列", handleTaskCmd},
|
||||||
|
{"cancel", "取消任务", handleCancelCmd},
|
||||||
{"watch", "监听聊天(UserBot)", handleWatchCmd},
|
{"watch", "监听聊天(UserBot)", handleWatchCmd},
|
||||||
{"unwatch", "取消监听聊天(UserBot)", handleUnwatchCmd},
|
{"unwatch", "取消监听聊天(UserBot)", handleUnwatchCmd},
|
||||||
{"lswatch", "列出监听的聊天(UserBot)", handleLswatchCmd},
|
{"lswatch", "列出监听的聊天(UserBot)", handleLswatchCmd},
|
||||||
{"save", "保存文件", handleSilentMode(handleSaveCmd, handleSilentSaveReplied)},
|
|
||||||
{"config", "修改配置", handleConfigCmd},
|
{"config", "修改配置", handleConfigCmd},
|
||||||
{"fnametmpl", "设置文件命名模板", handleConfigFnameTmpl},
|
{"fnametmpl", "设置文件命名模板", handleConfigFnameTmpl},
|
||||||
{"update", "检查更新", handleUpdateCmd},
|
|
||||||
{"help", "显示帮助", handleHelpCmd},
|
{"help", "显示帮助", handleHelpCmd},
|
||||||
|
{"parser", "管理解析器", handleParserCmd},
|
||||||
|
{"update", "检查更新", handleUpdateCmd},
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(disp dispatcher.Dispatcher) {
|
func Register(disp dispatcher.Dispatcher) {
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/rule"
|
"github.com/krau/SaveAny-Bot/pkg/rule"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
args := strutil.ParseArgsRespectQuotes(update.EffectiveMessage.Text)
|
||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userChatID)
|
user, err := database.GetUserByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
@@ -25,7 +26,7 @@ import (
|
|||||||
|
|
||||||
func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Split(string(update.EffectiveMessage.Text), " ")
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
if len(args) >= 3 {
|
if len(args) >= 3 {
|
||||||
return handleBatchSave(ctx, update, args[1:])
|
return handleBatchSave(ctx, update, args[1:])
|
||||||
}
|
}
|
||||||
@@ -34,17 +35,6 @@ func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
// genFilename := func() string {
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// return args[1]
|
|
||||||
// }
|
|
||||||
// filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
|
||||||
// return filename
|
|
||||||
// }()
|
|
||||||
// option := tfile.WithNameIfEmpty(genFilename)
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// option = tfile.WithName(genFilename)
|
|
||||||
// }
|
|
||||||
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -75,29 +65,12 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if len(args) >= 3 {
|
if len(args) >= 3 {
|
||||||
return handleBatchSave(ctx, update, args[1:])
|
return handleBatchSave(ctx, update, args[1:])
|
||||||
}
|
}
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
if stor == nil {
|
|
||||||
logger.Warn("Context storage is nil")
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("未找到存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
replyTo := update.EffectiveMessage.ReplyToMessage
|
replyTo := update.EffectiveMessage.ReplyToMessage
|
||||||
if replyTo == nil || replyTo.Message == nil {
|
if replyTo == nil || replyTo.Message == nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
// genFilename := func() string {
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// return args[1]
|
|
||||||
// }
|
|
||||||
// filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
|
||||||
// return filename
|
|
||||||
// }()
|
|
||||||
// option := tfile.WithNameIfEmpty(genFilename)
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// option = tfile.WithName(genFilename)
|
|
||||||
// }
|
|
||||||
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -111,7 +84,7 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, update.GetUserChat().GetID(), stor, "", file, msg.GetID())
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, update.GetUserChat().GetID(), stor, dirutil.PathFromContext(ctx), file, msg.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
@@ -36,51 +37,71 @@ func handleSilentCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
||||||
dataid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
dataid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
||||||
data, ok := cache.Get[tcbdata.SetDefaultStorage](dataid)
|
data, ok := cache.Get[tcbdata.SetDefaultStorage](dataid)
|
||||||
if !ok {
|
|
||||||
|
failedAnswer := func(message string) error {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
QueryID: update.CallbackQuery.GetQueryID(),
|
||||||
Alert: true,
|
Alert: true,
|
||||||
Message: "数据已过期",
|
Message: message,
|
||||||
CacheTime: 5,
|
CacheTime: 5,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return failedAnswer("数据已过期")
|
||||||
|
}
|
||||||
userID := update.CallbackQuery.GetUserID()
|
userID := update.CallbackQuery.GetUserID()
|
||||||
|
|
||||||
storageName := data.StorageName
|
storageName := data.StorageName
|
||||||
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("存储获取失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
|
||||||
Alert: true,
|
|
||||||
Message: "存储获取失败: " + err.Error(),
|
|
||||||
CacheTime: 5,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("获取用户信息失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
}
|
||||||
Alert: true,
|
var dir *database.Dir
|
||||||
Message: "获取用户信息失败: " + err.Error(),
|
if data.DirID != 0 {
|
||||||
CacheTime: 5,
|
// 已经选择了文件夹
|
||||||
})
|
var err error
|
||||||
return dispatcher.EndGroups
|
dir, err = database.GetDirByID(ctx, data.DirID)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("获取文件夹信息失败: " + err.Error())
|
||||||
|
}
|
||||||
|
user.DefaultDir = dir.ID
|
||||||
|
} else {
|
||||||
|
// 检查是否有可用的文件夹
|
||||||
|
dirs, err := database.GetDirsByUserIDAndStorageName(ctx, user.ID, storageName)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("获取目录失败: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(dirs) > 0 {
|
||||||
|
// 要求选择文件夹
|
||||||
|
markup, err := msgelem.BuildSetDefaultDirMarkup(ctx, storageName, dirs)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("构建目录选择失败: " + err.Error())
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
Message: "请选择要保存到的默认文件夹",
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user.DefaultStorage = selectedStorage.Name()
|
user.DefaultStorage = selectedStorage.Name()
|
||||||
if err := database.UpdateUser(ctx, user); err != nil {
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("更新用户信息失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
}
|
||||||
Alert: true,
|
msg := fmt.Sprintf("已将默认存储位置设为: %s", selectedStorage.Name())
|
||||||
Message: "更新用户信息失败: " + err.Error(),
|
if dir != nil {
|
||||||
CacheTime: 5,
|
msg += fmt.Sprintf(":/%s", strings.TrimPrefix(dir.Path, "/"))
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: "已将默认存储位置设置为: " + selectedStorage.Name(),
|
Message: msg,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -92,7 +113,7 @@ func handleStorageCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, userID, storages)
|
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, storages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取存储失败: "+err.Error()), nil)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
113
client/bot/handlers/tasks.go
Normal file
113
client/bot/handlers/tasks.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleTaskCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
args := strings.Fields(update.EffectiveMessage.Text)
|
||||||
|
if len(args) == 1 {
|
||||||
|
showRunningTasks(ctx, update)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[1] {
|
||||||
|
case "running", "run", "r":
|
||||||
|
showRunningTasks(ctx, update)
|
||||||
|
case "queued", "queue", "q", "waiting":
|
||||||
|
showQueuedTasks(ctx, update)
|
||||||
|
case "cancel", "c":
|
||||||
|
if len(args) < 3 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("用法: /tasks cancel <task_id>"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
taskID := args[2]
|
||||||
|
if err := core.CancelTask(ctx, taskID); err != nil {
|
||||||
|
logger.Errorf("取消任务 %s 失败: %v", taskID, err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("取消任务失败: "+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray([]styling.StyledTextOption{
|
||||||
|
styling.Plain("已请求取消任务: "),
|
||||||
|
styling.Code(taskID),
|
||||||
|
}), nil)
|
||||||
|
default:
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("用法: /tasks [running|queued|cancel <task_id>]"), nil)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func showRunningTasks(ctx *ext.Context, update *ext.Update) {
|
||||||
|
tasks := core.GetRunningTasks(ctx)
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("当前没有正在运行的任务"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*4)
|
||||||
|
opts = append(opts,
|
||||||
|
styling.Bold("当前正在运行的任务:"),
|
||||||
|
styling.Plain(fmt.Sprintf("\n总数: %d\n", len(tasks))),
|
||||||
|
)
|
||||||
|
for _, t := range tasks {
|
||||||
|
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
||||||
|
status := "运行中"
|
||||||
|
if t.Cancelled {
|
||||||
|
status = "已请求取消"
|
||||||
|
}
|
||||||
|
opts = append(opts,
|
||||||
|
styling.Plain("\nID: "),
|
||||||
|
styling.Code(t.ID),
|
||||||
|
styling.Plain("\n名称: "),
|
||||||
|
styling.Code(t.Title),
|
||||||
|
styling.Plain("\n创建时间: "),
|
||||||
|
styling.Code(created),
|
||||||
|
styling.Plain("\n状态: "),
|
||||||
|
styling.Code(status),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(opts), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showQueuedTasks(ctx *ext.Context, update *ext.Update) {
|
||||||
|
tasks := core.GetQueuedTasks(ctx)
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("当前没有排队中的任务"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*3)
|
||||||
|
opts = append(opts,
|
||||||
|
styling.Bold("当前排队中的任务:"),
|
||||||
|
styling.Plain(fmt.Sprintf("\n总数: %d\n", len(tasks))),
|
||||||
|
)
|
||||||
|
for _, t := range tasks {
|
||||||
|
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
||||||
|
status := "排队中"
|
||||||
|
if t.Cancelled {
|
||||||
|
status = "已请求取消"
|
||||||
|
}
|
||||||
|
opts = append(opts,
|
||||||
|
styling.Plain("\nID: "),
|
||||||
|
styling.Code(t.ID),
|
||||||
|
styling.Plain("\n名称: "),
|
||||||
|
styling.Code(t.Title),
|
||||||
|
styling.Plain("\n创建时间: "),
|
||||||
|
styling.Code(created),
|
||||||
|
styling.Plain("\n状态: "),
|
||||||
|
styling.Code(status),
|
||||||
|
)
|
||||||
|
if len(tasks) > 10 {
|
||||||
|
opts = append(opts, styling.Plain("\n...\n只显示前 10 个任务, 共 "+fmt.Sprintf("%d", len(tasks))+" 个任务"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(opts), nil)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
@@ -59,18 +61,16 @@ func handleTelegraphUrlMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSilentSaveTelegraph(ctx *ext.Context, update *ext.Update) error {
|
func handleSilentSaveTelegraph(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
if stor == nil {
|
|
||||||
logger.Warn("Context storage is nil")
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("未找到存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
msg, result, err := shortcut.GetTphPicsFromMessageWithReply(ctx, update)
|
msg, result, err := shortcut.GetTphPicsFromMessageWithReply(ctx, update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, result.Page, result.TphDir, result.Pics, stor, msg.ID)
|
dirpath := result.TphDir
|
||||||
|
if p := dirutil.PathFromContext(ctx); p != "" {
|
||||||
|
dirpath = path.Join(p, dirpath)
|
||||||
|
}
|
||||||
|
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, result.Page, dirpath, result.Pics, stor, msg.ID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/html"
|
"github.com/gotd/td/telegram/message/html"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
"github.com/unvgo/ghselfupdate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
@@ -21,7 +21,7 @@ func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("You are in dev or the version var failed to inject: %v", err)), nil)
|
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("You are in dev or the version var failed to inject: %v", err)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
latest, ok, err := selfupdate.DetectLatest(config.GitRepo)
|
latest, ok, err := ghselfupdate.DetectLatest(config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("检测最新版本失败: %v", err)), nil)
|
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("检测最新版本失败: %v", err)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -30,10 +30,15 @@ func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
ctx.Reply(u, ext.ReplyTextString("没有找到版本信息"), nil)
|
ctx.Reply(u, ext.ReplyTextString("没有找到版本信息"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
if latest.Version.Major != currentV.Major {
|
||||||
|
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("检测到大版本更新: %s -> %s , 请前往 GitHub 手动下载最新版本并查看迁移指南", currentV, latest.Version)), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
if latest.Version.LT(currentV) || latest.Version.Equals(currentV) {
|
if latest.Version.LT(currentV) || latest.Version.Equals(currentV) {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("当前已经是最新版本: %s", config.Version)), nil)
|
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("当前已经是最新版本: %s", config.Version)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
indocker := config.Docker == "true"
|
||||||
ctx.Sender.To(u.GetUserChat().AsInputPeer()).StyledText(ctx, html.String(nil, func() string {
|
ctx.Sender.To(u.GetUserChat().AsInputPeer()).StyledText(ctx, html.String(nil, func() string {
|
||||||
md := latest.ReleaseNotes
|
md := latest.ReleaseNotes
|
||||||
md = regexp.MustCompile(`(?m)^###\s+ (.+)$`).ReplaceAllString(md, "<b>$1</b>")
|
md = regexp.MustCompile(`(?m)^###\s+ (.+)$`).ReplaceAllString(md, "<b>$1</b>")
|
||||||
@@ -49,6 +54,15 @@ func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
|
|
||||||
return `<blockquote expandable>` + md + `</blockquote>`
|
return `<blockquote expandable>` + md + `</blockquote>`
|
||||||
}()))
|
}()))
|
||||||
|
if indocker {
|
||||||
|
text := fmt.Sprintf("发现新版本: %s\n当前版本: %s\n发布时间: %s\n由于您正在使用 Docker 部署, 请自行在部署平台上执行更新命令",
|
||||||
|
latest.Version,
|
||||||
|
config.Version,
|
||||||
|
latest.PublishedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
)
|
||||||
|
ctx.Reply(u, ext.ReplyTextString(text), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
text := fmt.Sprintf(`发现新版本: %s
|
text := fmt.Sprintf(`发现新版本: %s
|
||||||
当前版本: %s
|
当前版本: %s
|
||||||
|
|
||||||
@@ -86,7 +100,7 @@ func handleUpdateCallback(ctx *ext.Context, u *ext.Update) error {
|
|||||||
ID: u.CallbackQuery.GetMsgID(),
|
ID: u.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("正在升级中, 当前版本: %s", config.Version),
|
Message: fmt.Sprintf("正在升级中, 当前版本: %s", config.Version),
|
||||||
})
|
})
|
||||||
latest, err := selfupdate.UpdateSelf(currentV, config.GitRepo)
|
latest, err := ghselfupdate.UpdateSelf(currentV, config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: u.CallbackQuery.GetMsgID(),
|
ID: u.CallbackQuery.GetMsgID(),
|
||||||
|
|||||||
37
client/bot/handlers/utils/dirutil/context.go
Normal file
37
client/bot/handlers/utils/dirutil/context.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dirutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
var dirContextKey = contextKey{}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context, dir *database.Dir) context.Context {
|
||||||
|
if dir == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, dirContextKey, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) *database.Dir {
|
||||||
|
dir, ok := ctx.Value(dirContextKey).(*database.Dir)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathFromContext returns the directory path stored in the context.
|
||||||
|
//
|
||||||
|
// If no directory is found, an empty string is returned.
|
||||||
|
func PathFromContext(ctx context.Context) string {
|
||||||
|
dir := FromContext(ctx)
|
||||||
|
if dir == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dir.Path
|
||||||
|
}
|
||||||
@@ -45,6 +45,8 @@ func BuildAddSelectStorageKeyboard(stors []storage.Storage, adddata tcbdata.Add)
|
|||||||
TphDirPath: adddata.TphDirPath,
|
TphDirPath: adddata.TphDirPath,
|
||||||
|
|
||||||
ParsedItem: adddata.ParsedItem,
|
ParsedItem: adddata.ParsedItem,
|
||||||
|
|
||||||
|
DirectLinks: adddata.DirectLinks,
|
||||||
}
|
}
|
||||||
dataid := xid.New().String()
|
dataid := xid.New().String()
|
||||||
err := cache.Set(dataid, data)
|
err := cache.Set(dataid, data)
|
||||||
@@ -94,7 +96,10 @@ func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storag
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSetDefaultStorageMarkup(ctx context.Context, userID int64, stors []storage.Storage) (*tg.ReplyInlineMarkup, error) {
|
// Builds the inline keyboard for setting default storage
|
||||||
|
func BuildSetDefaultStorageMarkup(
|
||||||
|
ctx context.Context,
|
||||||
|
stors []storage.Storage) (*tg.ReplyInlineMarkup, error) {
|
||||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
for _, storage := range stors {
|
for _, storage := range stors {
|
||||||
data := tcbdata.SetDefaultStorage{
|
data := tcbdata.SetDefaultStorage{
|
||||||
@@ -119,7 +124,35 @@ func BuildSetDefaultStorageMarkup(ctx context.Context, userID int64, stors []sto
|
|||||||
return markup, nil
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSetDirKeyboard(dirs []database.Dir, dataid string) (*tg.ReplyInlineMarkup, error) {
|
func BuildSetDefaultDirMarkup(ctx context.Context,
|
||||||
|
seletedStorage string,
|
||||||
|
dirs []database.Dir) (*tg.ReplyInlineMarkup, error) {
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dataid := xid.New().String()
|
||||||
|
data := tcbdata.SetDefaultStorage{
|
||||||
|
StorageName: seletedStorage,
|
||||||
|
DirID: dir.ID,
|
||||||
|
}
|
||||||
|
err := cache.Set(dataid, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: dir.Path,
|
||||||
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeSetDefault, dataid),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
|
for i := 0; i < len(buttons); i += 3 {
|
||||||
|
row := tg.KeyboardButtonRow{}
|
||||||
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
|
markup.Rows = append(markup.Rows, row)
|
||||||
|
}
|
||||||
|
return markup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSetDirMarkupForAdd(dirs []database.Dir, dataid string) (*tg.ReplyInlineMarkup, error) {
|
||||||
data, ok := cache.Get[tcbdata.Add](dataid)
|
data, ok := cache.Get[tcbdata.Add](dataid)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to get data from cache: %s", dataid)
|
return nil, fmt.Errorf("failed to get data from cache: %s", dataid)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (m matchedStorName) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// can we use this storage name directly?
|
// can we use this storage name directly?
|
||||||
func (m matchedStorName) IsUsable() bool {
|
func (m matchedStorName) Usable() bool {
|
||||||
return m != "" && m != rule.RuleStorNameChosen
|
return m != "" && m != rule.RuleStorNameChosen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
client/bot/handlers/utils/shortcut/directlinks.go
Normal file
30
client/bot/handlers/utils/shortcut/directlinks.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package shortcut
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
"github.com/krau/SaveAny-Bot/core/tasks/directlinks"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateAndAddDirectTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPath string, links []string, msgID int, userID int64) error {
|
||||||
|
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
||||||
|
task := directlinks.NewTask(xid.New().String(), injectCtx, links, stor, stor.JoinStoragePath(dirPath), directlinks.NewProgress(msgID, userID))
|
||||||
|
if err := core.AddTask(injectCtx, task); err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: msgID,
|
||||||
|
Message: "任务添加失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "任务已添加",
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
@@ -99,13 +99,6 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
logger.Debugf("message %d has no media", msg.GetID())
|
logger.Debugf("message %d has no media", msg.GetID())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// var opt tfile.TGFileOption
|
|
||||||
// switch user.FilenameStrategy {
|
|
||||||
// case fnamest.Message.String():
|
|
||||||
// opt = tfile.WithName(tgutil.GenFileNameFromMessage(*msg))
|
|
||||||
// default:
|
|
||||||
// opt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg))
|
|
||||||
// }
|
|
||||||
opts := mediautil.TfileOptions(ctx, user, msg)
|
opts := mediautil.TfileOptions(ctx, user, msg)
|
||||||
file, err := tfile.FromMediaMessage(media, client, msg, opts...)
|
file, err := tfile.FromMediaMessage(media, client, msg, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,12 +126,12 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
}
|
}
|
||||||
msg, err := tgutil.GetMessageByID(tctx, chatId, msgId)
|
msg, err := tgutil.GetMessageByID(tctx, chatId, msgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to get message by ID: %s", err)
|
logger.Error(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
groupID, isGroup := msg.GetGroupedID()
|
groupID, isGroup := msg.GetGroupedID()
|
||||||
if isGroup && groupID != 0 && !linkUrl.Query().Has("single") {
|
if isGroup && groupID != 0 && !linkUrl.Query().Has("single") {
|
||||||
gmsgs, err := tgutil.GetGroupedMessages(ctx, chatId, msg)
|
gmsgs, err := tgutil.GetGroupedMessages(tctx, chatId, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to get grouped messages: %s", err)
|
logger.Errorf("failed to get grouped messages: %s", err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
|
|||||||
if matchedDirPath != "" {
|
if matchedDirPath != "" {
|
||||||
dirPath = matchedDirPath.String()
|
dirPath = matchedDirPath.String()
|
||||||
}
|
}
|
||||||
if matchedStorageName.IsUsable() {
|
if matchedStorageName.Usable() {
|
||||||
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
||||||
@@ -111,7 +111,7 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
return stor.Name(), ruleutil.MatchedDirPath(dirPath)
|
return stor.Name(), ruleutil.MatchedDirPath(dirPath)
|
||||||
}
|
}
|
||||||
storname := storName.String()
|
storname := storName.String()
|
||||||
if !storName.IsUsable() {
|
if !storName.Usable() {
|
||||||
storname = stor.Name()
|
storname = stor.Name()
|
||||||
}
|
}
|
||||||
return storname, dirP
|
return storname, dirP
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
|||||||
goto startCreateTask
|
goto startCreateTask
|
||||||
}
|
}
|
||||||
dirPath = matchedDirPath.String()
|
dirPath = matchedDirPath.String()
|
||||||
if matchedStorageName.IsUsable() {
|
if matchedStorageName.Usable() {
|
||||||
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
||||||
|
|||||||
@@ -1,80 +1,57 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto"
|
"github.com/celestix/gotgproto"
|
||||||
"github.com/charmbracelet/huh"
|
"golang.org/x/term"
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
"github.com/fatih/color"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type terminalAuthConversator struct{}
|
type terminalAuthConversator struct{}
|
||||||
|
|
||||||
func (t *terminalAuthConversator) AskPhoneNumber() (string, error) {
|
func readLine(prompt string) (string, error) {
|
||||||
phone := ""
|
fmt.Print(prompt)
|
||||||
err := huh.NewInput().Title("Your Phone Number").
|
reader := bufio.NewReader(os.Stdin)
|
||||||
Placeholder("+44 123456").
|
text, err := reader.ReadString('\n')
|
||||||
Prompt("> ").
|
|
||||||
Value(&phone).
|
|
||||||
WithTheme(huh.ThemeCatppuccin()).
|
|
||||||
Run()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return strings.TrimSpace(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("Sending code to your phone number...")
|
func (t *terminalAuthConversator) AskPhoneNumber() (string, error) {
|
||||||
|
fmt.Println("Your Phone Number (e.g. +44 123456):")
|
||||||
return strings.TrimSpace(phone), nil
|
return readLine("> ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *terminalAuthConversator) AskCode() (string, error) {
|
func (t *terminalAuthConversator) AskCode() (string, error) {
|
||||||
code := ""
|
fmt.Println("Your Code (e.g. 123456):")
|
||||||
err := huh.NewInput().Title("Your Code").
|
return readLine("> ")
|
||||||
Placeholder("123456").
|
|
||||||
Value(&code).
|
|
||||||
Prompt("> ").
|
|
||||||
WithTheme(huh.ThemeCatppuccin()).
|
|
||||||
Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(code), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *terminalAuthConversator) AskPassword() (string, error) {
|
func (t *terminalAuthConversator) AskPassword() (string, error) {
|
||||||
pwd := ""
|
fmt.Println("Your 2FA Password:")
|
||||||
|
fmt.Print("> ")
|
||||||
err := huh.NewInput().Title("Your 2FA Password").
|
bytePwd, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
EchoMode(huh.EchoModePassword).
|
fmt.Println()
|
||||||
Value(&pwd).
|
|
||||||
Prompt("> ").
|
|
||||||
WithTheme(huh.ThemeCatppuccin()).
|
|
||||||
Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(pwd), nil
|
return strings.TrimSpace(string(bytePwd)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *terminalAuthConversator) AuthStatus(authStatus gotgproto.AuthStatus) {
|
func (t *terminalAuthConversator) AuthStatus(authStatus gotgproto.AuthStatus) {
|
||||||
switch authStatus.Event {
|
switch authStatus.Event {
|
||||||
case gotgproto.AuthStatusPhoneRetrial:
|
case gotgproto.AuthStatusPhoneRetrial:
|
||||||
color.Red("The phone number you just entered seems to be incorrect,")
|
fmt.Printf("The phone number is incorrect. Attempts left: %d\n", authStatus.AttemptsLeft)
|
||||||
color.Red("Attempts Left: %d", authStatus.AttemptsLeft)
|
|
||||||
color.Red("Please try again....")
|
|
||||||
case gotgproto.AuthStatusPasswordRetrial:
|
case gotgproto.AuthStatusPasswordRetrial:
|
||||||
color.Red("The 2FA password you just entered seems to be incorrect,")
|
fmt.Printf("The 2FA password is incorrect. Attempts left: %d\n", authStatus.AttemptsLeft)
|
||||||
color.Red("Attempts Left: %d", authStatus.AttemptsLeft)
|
|
||||||
color.Red("Please try again....")
|
|
||||||
case gotgproto.AuthStatusPhoneCodeRetrial:
|
case gotgproto.AuthStatusPhoneCodeRetrial:
|
||||||
color.Red("The OTP you just entered seems to be incorrect,")
|
fmt.Printf("The OTP code is incorrect. Attempts left: %d\n", authStatus.AttemptsLeft)
|
||||||
color.Red("Attempts Left: %d", authStatus.AttemptsLeft)
|
|
||||||
color.Red("Please try again....")
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ import (
|
|||||||
"github.com/celestix/gotgproto/sessionMaker"
|
"github.com/celestix/gotgproto/sessionMaker"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/telegram/dcs"
|
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/middleware"
|
"github.com/krau/SaveAny-Bot/client/middleware"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/ncruces/go-sqlite3/gormlite"
|
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var uc *gotgproto.Client
|
var uc *gotgproto.Client
|
||||||
@@ -53,28 +50,20 @@ func Login(ctx context.Context) (*gotgproto.Client, error) {
|
|||||||
err error
|
err error
|
||||||
})
|
})
|
||||||
go func() {
|
go func() {
|
||||||
var resolver dcs.Resolver
|
resolver, err := tgutil.NewConfigProxyResolver()
|
||||||
if config.C().Telegram.Proxy.Enable && config.C().Telegram.Proxy.URL != "" {
|
if err != nil {
|
||||||
dialer, err := netutil.NewProxyDialer(config.C().Telegram.Proxy.URL)
|
res <- struct {
|
||||||
if err != nil {
|
client *gotgproto.Client
|
||||||
res <- struct {
|
err error
|
||||||
client *gotgproto.Client
|
}{nil, err}
|
||||||
err error
|
return
|
||||||
}{nil, err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolver = dcs.Plain(dcs.PlainOptions{
|
|
||||||
Dial: dialer.(proxy.ContextDialer).DialContext,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolver = dcs.DefaultResolver()
|
|
||||||
}
|
}
|
||||||
tclient, err := gotgproto.NewClient(
|
tclient, err := gotgproto.NewClient(
|
||||||
config.C().Telegram.AppID,
|
config.C().Telegram.AppID,
|
||||||
config.C().Telegram.AppHash,
|
config.C().Telegram.AppHash,
|
||||||
gotgproto.ClientTypePhone(""),
|
gotgproto.ClientTypePhone(""),
|
||||||
&gotgproto.ClientOpts{
|
&gotgproto.ClientOpts{
|
||||||
Session: sessionMaker.SqlSession(gormlite.Open(config.C().Telegram.Userbot.Session)),
|
Session: sessionMaker.SqlSession(database.GetDialect(config.C().Telegram.Userbot.Session)),
|
||||||
AuthConversator: &terminalAuthConversator{},
|
AuthConversator: &terminalAuthConversator{},
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
DisableCopyright: true,
|
DisableCopyright: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
"github.com/unvgo/ghselfupdate"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -26,17 +26,32 @@ var upgradeCmd = &cobra.Command{
|
|||||||
Short: "Upgrade saveany-bot to the latest version",
|
Short: "Upgrade saveany-bot to the latest version",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
v := semver.MustParse(config.Version)
|
v := semver.MustParse(config.Version)
|
||||||
latest, err := selfupdate.UpdateSelf(v, config.GitRepo)
|
latest, found, err := ghselfupdate.DetectLatest(config.GitRepo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error occurred while detecting latest version:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Println("No releases found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if latest.Version.Major != v.Major {
|
||||||
|
fmt.Printf("Major version upgrade detected: %s -> %s. Please manually download the latest version and check the migration guide.\n", v, latest.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if latest.Version.Equals(v) || latest.Version.LT(v) {
|
||||||
|
fmt.Println("Current binary is the latest version", config.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Updating to version %s...\n", latest.Version)
|
||||||
|
latest, err = ghselfupdate.UpdateSelf(v, config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Update failed:", err)
|
fmt.Println("Update failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if latest.Version.Equals(v) {
|
fmt.Println("Successfully updated to version", latest.Version)
|
||||||
fmt.Println("Current binary is the latest version", config.Version)
|
fmt.Println("Release note:\n", latest.ReleaseNotes)
|
||||||
} else {
|
|
||||||
fmt.Println("Successfully updated to version", latest.Version)
|
|
||||||
fmt.Println("Release note:\n", latest.ReleaseNotes)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// [TODO] complete the i18n support
|
||||||
|
|
||||||
package i18n
|
package i18n
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ bot:
|
|||||||
/save [自定义文件名] - 保存文件
|
/save [自定义文件名] - 保存文件
|
||||||
/dir - 管理存储目录
|
/dir - 管理存储目录
|
||||||
/rule - 管理规则
|
/rule - 管理规则
|
||||||
|
/config - 修改配置
|
||||||
|
/fnametmpl - 设置文件自定义命名模板
|
||||||
|
/parser - 管理解析器插件
|
||||||
|
/watch - 监听聊天并自动保存 (UserBot)
|
||||||
/update - 检查更新并升级
|
/update - 检查更新并升级
|
||||||
|
|
||||||
使用帮助: https://sabot.unv.app/usage
|
使用帮助: https://sabot.unv.app/usage
|
||||||
@@ -57,6 +61,6 @@ bot:
|
|||||||
- [filter]: 可选, 格式为 过滤器类型:表达式 , 所有支持类型的过滤器请查看文档
|
- [filter]: 可选, 格式为 过滤器类型:表达式 , 所有支持类型的过滤器请查看文档
|
||||||
|
|
||||||
命令示例:
|
命令示例:
|
||||||
/watch 2229835658 msgre:.*plana.*
|
/watch -1002229835658 msgre:.*plana.*
|
||||||
|
|
||||||
这将监听 ID 为 2229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
这将监听 ID 为 -1002229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package tfile
|
package tdler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gotd/td/telegram/downloader"
|
"github.com/gotd/td/telegram/downloader"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/consts/tglimit"
|
"github.com/krau/SaveAny-Bot/pkg/consts/tglimit"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDownloader(file TGFile) *downloader.Builder {
|
func NewDownloader(file tfile.TGFile) *downloader.Builder {
|
||||||
return downloader.NewDownloader().WithPartSize(tglimit.MaxPartSize).
|
return downloader.NewDownloader().WithPartSize(tglimit.MaxPartSize).
|
||||||
Download(file.Dler(), file.Location()).WithThreads(dlutil.BestThreads(file.Size(), config.C().Threads))
|
Download(file.Dler(), file.Location()).WithThreads(dlutil.BestThreads(file.Size(), config.C().Threads))
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package ioutil
|
package ioutil
|
||||||
|
|
||||||
import "io"
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
type ProgressWriterAt struct {
|
type ProgressWriterAt struct {
|
||||||
wrAt io.WriterAt
|
wrAt io.WriterAt
|
||||||
@@ -46,4 +48,4 @@ func NewProgressWriter(
|
|||||||
wr: wr,
|
wr: wr,
|
||||||
onWrite: onWrite,
|
onWrite: onWrite,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,56 +7,24 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
|
||||||
url, err := url.Parse(proxyUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return proxy.FromURL(url, proxy.Direct)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProxyHTTPClient(proxyUrl string) (*http.Client, error) {
|
func NewProxyHTTPClient(proxyUrl string) (*http.Client, error) {
|
||||||
if proxyUrl == "" {
|
if proxyUrl == "" {
|
||||||
return &http.Client{
|
return http.DefaultClient, nil
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
transport, err := NewProxyTransport(proxyUrl)
|
||||||
u, err := url.Parse(proxyUrl)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &http.Client{
|
||||||
switch u.Scheme {
|
Transport: transport,
|
||||||
case "http", "https":
|
}, nil
|
||||||
return &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyURL(u),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
case "socks5":
|
|
||||||
dialer, err := proxy.FromURL(u, proxy.Direct)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialer.Dial(network, addr)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -76,3 +44,35 @@ func DefaultParserHTTPClient() *http.Client {
|
|||||||
})
|
})
|
||||||
return defaultProxyHttpClient
|
return defaultProxyHttpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewProxyTransport(proxyStr string) (*http.Transport, error) {
|
||||||
|
proxyURL, err := url.Parse(proxyStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport := &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
}
|
||||||
|
switch proxyURL.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
|
||||||
|
case "socks5", "socks5h":
|
||||||
|
dialer, err := proxy.FromURL(proxyURL, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.(proxy.ContextDialer).DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported proxy type: %s", proxyURL.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,3 +48,46 @@ func ParseIntStrRange(input string, sep string) (int64, int64, error) {
|
|||||||
}
|
}
|
||||||
return min, max, nil
|
return min, max, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseArgsRespectQuotes(input string) []string {
|
||||||
|
var args []string
|
||||||
|
var current strings.Builder
|
||||||
|
inQuotes := false
|
||||||
|
escaped := false
|
||||||
|
|
||||||
|
for _, r := range input {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
if r == '"' || r == '\\' {
|
||||||
|
current.WriteRune(r)
|
||||||
|
} else {
|
||||||
|
current.WriteRune('\\')
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
escaped = false
|
||||||
|
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
|
||||||
|
case r == '"':
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
|
||||||
|
case r == ' ' || r == '\t':
|
||||||
|
if inQuotes {
|
||||||
|
current.WriteRune(r)
|
||||||
|
} else if current.Len() > 0 {
|
||||||
|
args = append(args, current.String())
|
||||||
|
current.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Len() > 0 {
|
||||||
|
args = append(args, current.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|||||||
148
common/utils/strutil/string_test.go
Normal file
148
common/utils/strutil/string_test.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package strutil_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractTagsFromText(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
text string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
text: `初音ミクHappy 16th Birthday -Dear Creators-
|
||||||
|
✨エンドイラスト公開!✨
|
||||||
|
https://piapro.net/miku16thbd/
|
||||||
|
#初音ミク #miku16th`,
|
||||||
|
expected: []string{"初音ミク", "miku16th"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `ひっつきむし
|
||||||
|
#創作百合`,
|
||||||
|
expected: []string{"創作百合"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `#創作百合 #原创`,
|
||||||
|
expected: []string{"創作百合", "原创"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `プラニャ #ブルアカ`,
|
||||||
|
expected: []string{"ブルアカ"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `原神是一款#开放世界#冒险游戏,由中国著名游戏公司#miHoYo开发。`,
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := strutil.ExtractTagsFromText(test.text)
|
||||||
|
if !reflect.DeepEqual(result, test.expected) {
|
||||||
|
t.Fatalf("ExtractTagsFromText(%s) = %v, expected %v", test.text, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIntStrRange(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
sep string
|
||||||
|
wantMin int64
|
||||||
|
wantMax int64
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal range",
|
||||||
|
input: "10-20",
|
||||||
|
sep: "-",
|
||||||
|
wantMin: 10,
|
||||||
|
wantMax: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reverse order",
|
||||||
|
input: "30 - 10",
|
||||||
|
sep: "-",
|
||||||
|
wantMin: 10,
|
||||||
|
wantMax: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format",
|
||||||
|
input: "10",
|
||||||
|
sep: "-",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid number",
|
||||||
|
input: "a-b",
|
||||||
|
sep: "-",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
min, max, err := strutil.ParseIntStrRange(tt.input, tt.sep)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ParseIntStrRange(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr {
|
||||||
|
if min != tt.wantMin || max != tt.wantMax {
|
||||||
|
t.Errorf("ParseIntStrRange(%q) = (%d, %d), want (%d, %d)", tt.input, min, max, tt.wantMin, tt.wantMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseArgsRespectQuotes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple split",
|
||||||
|
input: `/rule add FILENAME-REGEX (?i)\.(mp4|mkv)$ "我的 Alist" /视频`,
|
||||||
|
want: []string{"/rule", "add", "FILENAME-REGEX", "(?i)\\.(mp4|mkv)$", "我的 Alist", "/视频"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped quotes",
|
||||||
|
input: `/rule add "My \"Awesome\" Folder"`,
|
||||||
|
want: []string{"/rule", "add", `My "Awesome" Folder`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped backslash",
|
||||||
|
input: `/cmd "C:\\Users\\Admin" test`,
|
||||||
|
want: []string{"/cmd", `C:\Users\Admin`, "test"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple quoted parts",
|
||||||
|
input: `"Hello World" "你好 世界"`,
|
||||||
|
want: []string{"Hello World", "你好 世界"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unquoted words",
|
||||||
|
input: "a b c",
|
||||||
|
want: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed quotes and plain",
|
||||||
|
input: `cmd "quoted arg" plain`,
|
||||||
|
want: []string{"cmd", "quoted arg", "plain"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := strutil.ParseArgsRespectQuotes(tt.input)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("ParseArgsRespectQuotes(%q) = %#v, want %#v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
|
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/duke-git/lancet/v2/maputil"
|
"github.com/duke-git/lancet/v2/maputil"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/mathutil"
|
"github.com/duke-git/lancet/v2/mathutil"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
lcstrutil "github.com/duke-git/lancet/v2/strutil"
|
lcstrutil "github.com/duke-git/lancet/v2/strutil"
|
||||||
"github.com/duke-git/lancet/v2/validator"
|
"github.com/duke-git/lancet/v2/validator"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/gotd/td/constant"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common/cache"
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
@@ -112,6 +112,31 @@ func InputMessageClassSliceFromInt(ids []int) []tg.InputMessageClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.Message, error) {
|
func GetMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.Message, error) {
|
||||||
|
if msg, err := getMessagesRange(ctx, chatID, minId, maxId); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
in := constant.TDLibPeerID(chatID)
|
||||||
|
plain := in.ToPlain()
|
||||||
|
|
||||||
|
var channel constant.TDLibPeerID
|
||||||
|
channel.Channel(plain)
|
||||||
|
if msg, err := getMessagesRange(ctx, int64(channel), minId, maxId); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
var userID constant.TDLibPeerID
|
||||||
|
userID.User(plain)
|
||||||
|
if msg, err := getMessagesRange(ctx, int64(userID), minId, maxId); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
var chat constant.TDLibPeerID
|
||||||
|
chat.Chat(plain)
|
||||||
|
if msg, err := getMessagesRange(ctx, int64(chat), minId, maxId); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get messages range for chatID %d", chatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.Message, error) {
|
||||||
if minId > maxId {
|
if minId > maxId {
|
||||||
return nil, fmt.Errorf("minId (%d) cannot be greater than maxId (%d)", minId, maxId)
|
return nil, fmt.Errorf("minId (%d) cannot be greater than maxId (%d)", minId, maxId)
|
||||||
}
|
}
|
||||||
@@ -167,97 +192,98 @@ func GetMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.M
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageItem struct {
|
// [TODO]
|
||||||
Message *tg.Message
|
// type MessageItem struct {
|
||||||
Error error
|
// Message *tg.Message
|
||||||
}
|
// Error error
|
||||||
|
// }
|
||||||
|
|
||||||
func IterMessages(ctx *ext.Context, chatID int64, minId, maxId int) (<-chan MessageItem, error) {
|
// func IterMessages(ctx *ext.Context, chatID int64, minId, maxId int) (<-chan MessageItem, error) {
|
||||||
total := maxId - minId + 1
|
// total := maxId - minId + 1
|
||||||
ch := make(chan MessageItem, 100)
|
// ch := make(chan MessageItem, 100)
|
||||||
|
|
||||||
go func() {
|
// go func() {
|
||||||
defer close(ch)
|
// defer close(ch)
|
||||||
if !ctx.Self.Bot {
|
// if !ctx.Self.Bot {
|
||||||
perr := ctx.PeerStorage.GetInputPeerById(chatID)
|
// perr := ctx.PeerStorage.GetInputPeerById(chatID)
|
||||||
if perr == nil || perr.(*tg.InputPeerEmpty) != nil {
|
// if perr == nil || perr.(*tg.InputPeerEmpty) != nil {
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Error: fmt.Errorf("peer not found: %d", chatID),
|
// Error: fmt.Errorf("peer not found: %d", chatID),
|
||||||
}
|
// }
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
for i := 0; i < total; i += 100 {
|
// for i := 0; i < total; i += 100 {
|
||||||
start := minId + i
|
// start := minId + i
|
||||||
end := min(start+100, maxId)
|
// end := min(start+100, maxId)
|
||||||
msgs, err := ctx.Raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
// msgs, err := ctx.Raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||||
Peer: perr,
|
// Peer: perr,
|
||||||
OffsetID: start,
|
// OffsetID: start,
|
||||||
AddOffset: start - end,
|
// AddOffset: start - end,
|
||||||
Limit: 100,
|
// Limit: 100,
|
||||||
})
|
// })
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Error: fmt.Errorf("failed to get messages: %w", err),
|
// Error: fmt.Errorf("failed to get messages: %w", err),
|
||||||
}
|
// }
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
var msgClass []tg.MessageClass
|
// var msgClass []tg.MessageClass
|
||||||
switch msgsv := msgs.(type) {
|
// switch msgsv := msgs.(type) {
|
||||||
case *tg.MessagesMessages:
|
// case *tg.MessagesMessages:
|
||||||
msgClass = msgsv.GetMessages()
|
// msgClass = msgsv.GetMessages()
|
||||||
case *tg.MessagesMessagesSlice:
|
// case *tg.MessagesMessagesSlice:
|
||||||
msgClass = msgsv.GetMessages()
|
// msgClass = msgsv.GetMessages()
|
||||||
case *tg.MessagesChannelMessages:
|
// case *tg.MessagesChannelMessages:
|
||||||
msgClass = msgsv.GetMessages()
|
// msgClass = msgsv.GetMessages()
|
||||||
default:
|
// default:
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Error: fmt.Errorf("unsupported message type: %T", msgsv),
|
// Error: fmt.Errorf("unsupported message type: %T", msgsv),
|
||||||
}
|
// }
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
for _, msg := range msgClass {
|
// for _, msg := range msgClass {
|
||||||
msg, ok := msg.AsNotEmpty()
|
// msg, ok := msg.AsNotEmpty()
|
||||||
if !ok {
|
// if !ok {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
switch msg := msg.(type) {
|
// switch msg := msg.(type) {
|
||||||
case *tg.Message:
|
// case *tg.Message:
|
||||||
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msg.GetID())
|
// key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msg.GetID())
|
||||||
cache.Set(key, msg)
|
// cache.Set(key, msg)
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Message: msg,
|
// Message: msg,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
for i := 0; i < total; i += 100 {
|
// for i := 0; i < total; i += 100 {
|
||||||
start := minId + i
|
// start := minId + i
|
||||||
end := min(start+100, maxId)
|
// end := min(start+100, maxId)
|
||||||
msgs, err := GetMessagesRange(ctx, chatID, start, end)
|
// msgs, err := GetMessagesRange(ctx, chatID, start, end)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Error: fmt.Errorf("failed to get messages: %w", err),
|
// Error: fmt.Errorf("failed to get messages: %w", err),
|
||||||
}
|
// }
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
for _, msg := range msgs {
|
// for _, msg := range msgs {
|
||||||
if msg == nil {
|
// if msg == nil {
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
ch <- MessageItem{
|
// ch <- MessageItem{
|
||||||
Message: msg,
|
// Message: msg,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}()
|
// }()
|
||||||
|
|
||||||
return ch, nil
|
// return ch, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, error) {
|
func getMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, error) {
|
||||||
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msgID)
|
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msgID)
|
||||||
if msg, ok := cache.Get[*tg.Message](key); ok {
|
if msg, ok := cache.Get[*tg.Message](key); ok {
|
||||||
return msg, nil
|
return msg, nil
|
||||||
@@ -280,6 +306,33 @@ func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, err
|
|||||||
return tgm, nil
|
return tgm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// f**k gotgproto's breaking changes
|
||||||
|
func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, error) {
|
||||||
|
// we don't know what the input chatID is bot api style(e.g. channel with -100 prefix) or plain tdlib style(no any prefix and every id is positive)
|
||||||
|
if msg, err := getMessageByID(ctx, chatID, msgID); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
in := constant.TDLibPeerID(chatID)
|
||||||
|
plain := in.ToPlain()
|
||||||
|
var channel constant.TDLibPeerID
|
||||||
|
channel.Channel(plain)
|
||||||
|
if msg, err := getMessageByID(ctx, int64(channel), msgID); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
var chat constant.TDLibPeerID
|
||||||
|
chat.Chat(plain)
|
||||||
|
if msg, err := getMessageByID(ctx, int64(chat), msgID); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
var userID constant.TDLibPeerID
|
||||||
|
userID.User(plain)
|
||||||
|
if msg, err := getMessageByID(ctx, int64(userID), msgID); err == nil {
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to get message by ID: chatID=%d, msgID=%d", chatID, msgID)
|
||||||
|
}
|
||||||
|
|
||||||
func GetGroupedMessages(ctx *ext.Context, chatID int64, msg *tg.Message) ([]*tg.Message, error) {
|
func GetGroupedMessages(ctx *ext.Context, chatID int64, msg *tg.Message) ([]*tg.Message, error) {
|
||||||
groupID, isGroup := msg.GetGroupedID()
|
groupID, isGroup := msg.GetGroupedID()
|
||||||
if !isGroup || groupID == 0 {
|
if !isGroup || groupID == 0 {
|
||||||
@@ -293,7 +346,7 @@ func GetGroupedMessages(ctx *ext.Context, chatID int64, msg *tg.Message) ([]*tg.
|
|||||||
}
|
}
|
||||||
msgs, err := GetMessagesRange(ctx, chatID, minID, maxID)
|
msgs, err := GetMessagesRange(ctx, chatID, minID, maxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get grouped messages: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
groupedMessages := make([]*tg.Message, 0, len(msgs))
|
groupedMessages := make([]*tg.Message, 0, len(msgs))
|
||||||
for _, m := range msgs {
|
for _, m := range msgs {
|
||||||
|
|||||||
41
common/utils/tgutil/net.go
Normal file
41
common/utils/tgutil/net.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package tgutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gotd/td/telegram/dcs"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
||||||
|
url, err := url.Parse(proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return proxy.FromURL(url, proxy.Direct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigProxyResolver() (dcs.Resolver, error) {
|
||||||
|
resolver := dcs.DefaultResolver()
|
||||||
|
if config.C().Proxy != "" {
|
||||||
|
// gloabl proxy, which has lower priority
|
||||||
|
dialer, err := newProxyDialer(config.C().Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolver = dcs.Plain(dcs.PlainOptions{
|
||||||
|
Dial: dialer.(proxy.ContextDialer).DialContext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if config.C().Telegram.Proxy.Enable && config.C().Telegram.Proxy.URL != "" {
|
||||||
|
dialer, err := newProxyDialer(config.C().Telegram.Proxy.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolver = dcs.Plain(dcs.PlainOptions{
|
||||||
|
Dial: dialer.(proxy.ContextDialer).DialContext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resolver, nil
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
func ParseChatID(ctx *ext.Context, idOrUsername string) (int64, error) {
|
func ParseChatID(ctx *ext.Context, idOrUsername string) (int64, error) {
|
||||||
idOrUsername = strings.TrimPrefix(idOrUsername, "@")
|
idOrUsername = strings.TrimPrefix(idOrUsername, "@")
|
||||||
if validator.IsIntStr(idOrUsername) {
|
if validator.IsIntStr(idOrUsername) {
|
||||||
chatID, err := strconv.Atoi(strings.TrimPrefix(idOrUsername, "-100"))
|
chatID, err := strconv.Atoi(idOrUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ url = "socks5://127.0.0.1:7890"
|
|||||||
[[storages]]
|
[[storages]]
|
||||||
# 标识名, 需要唯一
|
# 标识名, 需要唯一
|
||||||
name = "本机1"
|
name = "本机1"
|
||||||
# 存储类型, 目前可用: local, alist, webdav, minio, telegram
|
# 存储类型, 目前可用: local, alist, webdav, s3, telegram
|
||||||
type = "local"
|
type = "local"
|
||||||
# 启用存储
|
# 启用存储
|
||||||
enable = true
|
enable = true
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ var storageFactories = map[storenum.StorageType]func(cfg *BaseConfig) (StorageCo
|
|||||||
storenum.Alist: createStorageConfig(&AlistStorageConfig{}),
|
storenum.Alist: createStorageConfig(&AlistStorageConfig{}),
|
||||||
storenum.Webdav: createStorageConfig(&WebdavStorageConfig{}),
|
storenum.Webdav: createStorageConfig(&WebdavStorageConfig{}),
|
||||||
storenum.Minio: createStorageConfig(&MinioStorageConfig{}),
|
storenum.Minio: createStorageConfig(&MinioStorageConfig{}),
|
||||||
|
storenum.S3: createStorageConfig(&S3StorageConfig{}),
|
||||||
storenum.Telegram: createStorageConfig(&TelegramStorageConfig{}),
|
storenum.Telegram: createStorageConfig(&TelegramStorageConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
config/storage/s3.go
Normal file
43
config/storage/s3.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3StorageConfig struct {
|
||||||
|
BaseConfig
|
||||||
|
Endpoint string `toml:"endpoint" mapstructure:"endpoint" json:"endpoint"`
|
||||||
|
AccessKeyID string `toml:"access_key_id" mapstructure:"access_key_id" json:"access_key_id"`
|
||||||
|
SecretAccessKey string `toml:"secret_access_key" mapstructure:"secret_access_key" json:"secret_access_key"`
|
||||||
|
BucketName string `toml:"bucket_name" mapstructure:"bucket_name" json:"bucket_name"`
|
||||||
|
UseSSL bool `toml:"use_ssl" mapstructure:"use_ssl" json:"use_ssl"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
Region string `toml:"region" mapstructure:"region" json:"region"`
|
||||||
|
VirtualHost bool `toml:"virtual_host" mapstructure:"virtual_host" json:"virtual_host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *S3StorageConfig) Validate() error {
|
||||||
|
if m.Endpoint == "" {
|
||||||
|
return fmt.Errorf("endpoint is required for s3 storage")
|
||||||
|
}
|
||||||
|
if m.AccessKeyID == "" || m.SecretAccessKey == "" {
|
||||||
|
return fmt.Errorf("access_key_id and secret_access_key are required for s3 storage")
|
||||||
|
}
|
||||||
|
if m.BucketName == "" {
|
||||||
|
return fmt.Errorf("bucket_name is required for s3 storage")
|
||||||
|
}
|
||||||
|
if m.BasePath == "" {
|
||||||
|
return fmt.Errorf("base_path is required for s3 storage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *S3StorageConfig) GetType() storenum.StorageType {
|
||||||
|
return storenum.S3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *S3StorageConfig) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
@@ -12,6 +12,11 @@ type TelegramStorageConfig struct {
|
|||||||
ForceFile bool `toml:"force_file" mapstructure:"force_file" json:"force_file"`
|
ForceFile bool `toml:"force_file" mapstructure:"force_file" json:"force_file"`
|
||||||
RateLimit int `toml:"rate_limit" mapstructure:"rate_limit" json:"rate_limit"`
|
RateLimit int `toml:"rate_limit" mapstructure:"rate_limit" json:"rate_limit"`
|
||||||
RateBurst int `toml:"rate_burst" mapstructure:"rate_burst" json:"rate_burst"`
|
RateBurst int `toml:"rate_burst" mapstructure:"rate_burst" json:"rate_burst"`
|
||||||
|
SkipLarge bool `toml:"skip_large" mapstructure:"skip_large" json:"skip_large"` // skip files larger than Telegram limit(2GB)
|
||||||
|
// split files larger than Telegram limit(2GB) into parts of specified size, in MB, leave 0 to set default(2000MB)
|
||||||
|
// only effective when SkipLarge is false
|
||||||
|
// use zip when splitting
|
||||||
|
SplitSizeMB int64 `toml:"split_size_mb" mapstructure:"split_size_mb" json:"split_size_mb"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TelegramStorageConfig) Validate() error {
|
func (m *TelegramStorageConfig) Validate() error {
|
||||||
|
|||||||
13
config/tg.go
13
config/tg.go
@@ -1,12 +1,13 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type telegramConfig struct {
|
type telegramConfig struct {
|
||||||
Token string `toml:"token" mapstructure:"token"`
|
Token string `toml:"token" mapstructure:"token"`
|
||||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||||
Proxy tgProxyConfig `toml:"proxy" mapstructure:"proxy"`
|
Proxy tgProxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||||
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
||||||
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"`
|
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"`
|
||||||
|
MediaGroupTimeout int `toml:"media_group_timeout" mapstructure:"media_group_timeout" json:"media_group_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userbotConfig struct {
|
type userbotConfig struct {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ var (
|
|||||||
Version string = "dev"
|
Version string = "dev"
|
||||||
BuildTime string = "unknown"
|
BuildTime string = "unknown"
|
||||||
GitCommit string = "unknown"
|
GitCommit string = "unknown"
|
||||||
|
Docker string = "false" // whether built inside Docker
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GitRepo = "krau/SaveAny-Bot"
|
GitRepo = "krau/SaveAny-Bot"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config/storage"
|
"github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -20,6 +25,7 @@ type Config struct {
|
|||||||
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache" json:"no_clean_cache"`
|
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache" json:"no_clean_cache"`
|
||||||
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"`
|
||||||
|
Proxy string `toml:"proxy" mapstructure:"proxy" json:"proxy"`
|
||||||
|
|
||||||
Cache cacheConfig `toml:"cache" mapstructure:"cache" json:"cache"`
|
Cache cacheConfig `toml:"cache" mapstructure:"cache" json:"cache"`
|
||||||
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||||
@@ -147,5 +153,43 @@ func Init(ctx context.Context) error {
|
|||||||
userStorages[user.ID] = user.Storages
|
userStorages[user.ID] = user.Storages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.Proxy != "" {
|
||||||
|
http.DefaultTransport, err = newProxyTransport(cfg.Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create proxy transport: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newProxyTransport(proxyStr string) (*http.Transport, error) {
|
||||||
|
proxyURL, err := url.Parse(proxyStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport := &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
}
|
||||||
|
switch proxyURL.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
|
||||||
|
case "socks5", "socks5h":
|
||||||
|
dialer, err := proxy.FromURL(proxyURL, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.(proxy.ContextDialer).DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported proxy type: %s", proxyURL.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport, nil
|
||||||
|
}
|
||||||
|
|||||||
44
core/core.go
44
core/core.go
@@ -10,15 +10,16 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/pkg/queue"
|
"github.com/krau/SaveAny-Bot/pkg/queue"
|
||||||
)
|
)
|
||||||
|
|
||||||
var queueInstance *queue.TaskQueue[Exectable]
|
var queueInstance *queue.TaskQueue[Executable]
|
||||||
|
|
||||||
type Exectable interface {
|
type Executable interface {
|
||||||
Type() tasktype.TaskType
|
Type() tasktype.TaskType
|
||||||
|
Title() string
|
||||||
TaskID() string
|
TaskID() string
|
||||||
Execute(ctx context.Context) error
|
Execute(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func worker(ctx context.Context, qe *queue.TaskQueue[Exectable], semaphore chan struct{}) {
|
func worker(ctx context.Context, qe *queue.TaskQueue[Executable], semaphore chan struct{}) {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
execHooks := config.C().Hook.Exec
|
execHooks := config.C().Hook.Exec
|
||||||
for {
|
for {
|
||||||
@@ -28,27 +29,27 @@ func worker(ctx context.Context, qe *queue.TaskQueue[Exectable], semaphore chan
|
|||||||
logger.Error("Failed to get task from queue:", err)
|
logger.Error("Failed to get task from queue:", err)
|
||||||
break // queue closed and empty
|
break // queue closed and empty
|
||||||
}
|
}
|
||||||
task := qtask.Data
|
exe := qtask.Data
|
||||||
logger.Infof("Processing task: %s", task.TaskID())
|
logger.Infof("Processing task: %s", exe.TaskID())
|
||||||
if err := ExecCommandString(qtask.Context(), execHooks.TaskBeforeStart); err != nil {
|
if err := ExecCommandString(qtask.Context(), execHooks.TaskBeforeStart); err != nil {
|
||||||
logger.Errorf("Failed to execute before start hook for task %s: %v", task.TaskID(), err)
|
logger.Errorf("Failed to execute before start hook for task %s: %v", exe.TaskID(), err)
|
||||||
}
|
}
|
||||||
if err := task.Execute(qtask.Context()); err != nil {
|
if err := exe.Execute(qtask.Context()); err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
logger.Infof("Task %s was canceled", task.TaskID())
|
logger.Infof("Task %s was canceled", exe.TaskID())
|
||||||
if err := ExecCommandString(ctx, execHooks.TaskCancel); err != nil {
|
if err := ExecCommandString(ctx, execHooks.TaskCancel); err != nil {
|
||||||
logger.Errorf("Failed to execute cancel hook for task %s: %v", task.TaskID(), err)
|
logger.Errorf("Failed to execute cancel hook for task %s: %v", exe.TaskID(), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Errorf("Failed to execute task %s: %v", task.TaskID(), err)
|
logger.Errorf("Failed to execute task %s: %v", exe.TaskID(), err)
|
||||||
if err := ExecCommandString(ctx, execHooks.TaskFail); err != nil {
|
if err := ExecCommandString(ctx, execHooks.TaskFail); err != nil {
|
||||||
logger.Errorf("Failed to execute fail hook for task %s: %v", task.TaskID(), err)
|
logger.Errorf("Failed to execute fail hook for task %s: %v", exe.TaskID(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Task %s completed successfully", task.TaskID())
|
logger.Infof("Task %s completed successfully", exe.TaskID())
|
||||||
if err := ExecCommandString(ctx, execHooks.TaskSuccess); err != nil {
|
if err := ExecCommandString(ctx, execHooks.TaskSuccess); err != nil {
|
||||||
logger.Errorf("Failed to execute success hook for task %s: %v", task.TaskID(), err)
|
logger.Errorf("Failed to execute success hook for task %s: %v", exe.TaskID(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
qe.Done(qtask.ID)
|
qe.Done(qtask.ID)
|
||||||
@@ -60,7 +61,7 @@ func Run(ctx context.Context) {
|
|||||||
log.FromContext(ctx).Info("Start processing tasks...")
|
log.FromContext(ctx).Info("Start processing tasks...")
|
||||||
semaphore := make(chan struct{}, config.C().Workers)
|
semaphore := make(chan struct{}, config.C().Workers)
|
||||||
if queueInstance == nil {
|
if queueInstance == nil {
|
||||||
queueInstance = queue.NewTaskQueue[Exectable]()
|
queueInstance = queue.NewTaskQueue[Executable]()
|
||||||
}
|
}
|
||||||
for range config.C().Workers {
|
for range config.C().Workers {
|
||||||
go worker(ctx, queueInstance, semaphore)
|
go worker(ctx, queueInstance, semaphore)
|
||||||
@@ -68,8 +69,8 @@ func Run(ctx context.Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTask(ctx context.Context, task Exectable) error {
|
func AddTask(ctx context.Context, task Executable) error {
|
||||||
return queueInstance.Add(queue.NewTask(ctx, task.TaskID(), task))
|
return queueInstance.Add(queue.NewTask(ctx, task.TaskID(), task.Title(), task))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CancelTask(ctx context.Context, id string) error {
|
func CancelTask(ctx context.Context, id string) error {
|
||||||
@@ -78,8 +79,13 @@ func CancelTask(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetLength(ctx context.Context) int {
|
func GetLength(ctx context.Context) int {
|
||||||
if queueInstance == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return queueInstance.ActiveLength()
|
return queueInstance.ActiveLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRunningTasks(ctx context.Context) []queue.TaskInfo {
|
||||||
|
return queueInstance.RunningTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQueuedTasks(ctx context.Context) []queue.TaskInfo {
|
||||||
|
return queueInstance.QueuedTasks()
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/duke-git/lancet/v2/retry"
|
"github.com/duke-git/lancet/v2/retry"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func (t *Task) Execute(ctx context.Context) error {
|
|||||||
workers := config.C().Workers
|
workers := config.C().Workers
|
||||||
eg, gctx := errgroup.WithContext(ctx)
|
eg, gctx := errgroup.WithContext(ctx)
|
||||||
eg.SetLimit(workers)
|
eg.SetLimit(workers)
|
||||||
for _, elem := range t.Elems {
|
for _, elem := range t.elems {
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
t.processingMu.RLock()
|
t.processingMu.RLock()
|
||||||
if t.processing[elem.ID] != nil {
|
if t.processing[elem.ID] != nil {
|
||||||
@@ -68,7 +68,7 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error {
|
|||||||
errg.Go(func() error {
|
errg.Go(func() error {
|
||||||
defer pw.Close()
|
defer pw.Close()
|
||||||
logger.Info("Starting file download in stream mode")
|
logger.Info("Starting file download in stream mode")
|
||||||
_, err := tfile.NewDownloader(elem.File).Stream(uploadCtx, wr)
|
_, err := tdler.NewDownloader(elem.File).Stream(uploadCtx, wr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to download file: %v", err)
|
logger.Errorf("Failed to download file: %v", err)
|
||||||
pw.CloseWithError(err)
|
pw.CloseWithError(err)
|
||||||
@@ -95,7 +95,7 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error {
|
|||||||
t.downloaded.Add(int64(n))
|
t.downloaded.Add(int64(n))
|
||||||
t.Progress.OnProgress(ctx, t)
|
t.Progress.OnProgress(ctx, t)
|
||||||
})
|
})
|
||||||
_, err = tfile.NewDownloader(elem.File).Parallel(ctx, wrAt)
|
_, err = tdler.NewDownloader(elem.File).Parallel(ctx, wrAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download file: %w", err)
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ core.Executable = (*Task)(nil)
|
||||||
|
|
||||||
type TaskElement struct {
|
type TaskElement struct {
|
||||||
ID string
|
ID string
|
||||||
Storage storage.Storage
|
Storage storage.Storage
|
||||||
@@ -25,8 +28,8 @@ type TaskElement struct {
|
|||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string
|
ID string
|
||||||
Ctx context.Context
|
ctx context.Context
|
||||||
Elems []TaskElement
|
elems []TaskElement
|
||||||
Progress ProgressTracker
|
Progress ProgressTracker
|
||||||
IgnoreErrors bool // if true, errors during processing will be ignored
|
IgnoreErrors bool // if true, errors during processing will be ignored
|
||||||
downloaded atomic.Int64
|
downloaded atomic.Int64
|
||||||
@@ -36,6 +39,11 @@ type Task struct {
|
|||||||
failed map[string]error // [TODO] errors for each element
|
failed map[string]error // [TODO] errors for each element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title implements core.Exectable.
|
||||||
|
func (t *Task) Title() string {
|
||||||
|
return fmt.Sprintf("[%s](%d files/%.2fMB)", t.Type(), len(t.elems), float64(t.totalSize)/(1024*1024))
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) Type() tasktype.TaskType {
|
func (t *Task) Type() tasktype.TaskType {
|
||||||
return tasktype.TaskTypeTgfiles
|
return tasktype.TaskTypeTgfiles
|
||||||
}
|
}
|
||||||
@@ -78,8 +86,8 @@ func NewBatchTGFileTask(
|
|||||||
) *Task {
|
) *Task {
|
||||||
task := &Task{
|
task := &Task{
|
||||||
ID: id,
|
ID: id,
|
||||||
Ctx: ctx,
|
ctx: ctx,
|
||||||
Elems: files,
|
elems: files,
|
||||||
Progress: progress,
|
Progress: progress,
|
||||||
downloaded: atomic.Int64{},
|
downloaded: atomic.Int64{},
|
||||||
totalSize: func() int64 {
|
totalSize: func() int64 {
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ func (t *Task) Downloaded() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) Count() int {
|
func (t *Task) Count() int {
|
||||||
return len(t.Elems)
|
return len(t.elems)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) Processing() []TaskElementInfo {
|
func (t *Task) Processing() []TaskElementInfo {
|
||||||
processing := make([]TaskElementInfo, 0, len(t.Elems))
|
processing := make([]TaskElementInfo, 0, len(t.elems))
|
||||||
for _, elem := range t.processing {
|
for _, elem := range t.processing {
|
||||||
processing = append(processing, elem)
|
processing = append(processing, elem)
|
||||||
}
|
}
|
||||||
|
|||||||
167
core/tasks/directlinks/execute.go
Normal file
167
core/tasks/directlinks/execute.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package directlinks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/duke-git/lancet/v2/retry"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Task) Execute(ctx context.Context) error {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
logger.Infof("Starting directlinks task %s", t.ID)
|
||||||
|
if t.Progress != nil {
|
||||||
|
t.Progress.OnStart(ctx, t)
|
||||||
|
}
|
||||||
|
// head all links to get file info
|
||||||
|
eg, gctx := errgroup.WithContext(ctx)
|
||||||
|
eg.SetLimit(config.C().Workers)
|
||||||
|
fetchedTotalBytes := atomic.Int64{}
|
||||||
|
for _, file := range t.files {
|
||||||
|
eg.Go(func() error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, file.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create HEAD request for %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
resp, err := t.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to HEAD %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("HEAD %s returned status %d", file.URL, resp.StatusCode)
|
||||||
|
}
|
||||||
|
fetchedTotalBytes.Add(resp.ContentLength)
|
||||||
|
file.Size = resp.ContentLength
|
||||||
|
if name := resp.Header.Get("Content-Disposition"); name != "" {
|
||||||
|
// Set file name
|
||||||
|
filename := parseFilename(name)
|
||||||
|
file.Name = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err := eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error during HEAD requests: %v", err)
|
||||||
|
if t.Progress != nil {
|
||||||
|
t.Progress.OnDone(ctx, t, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.totalBytes = fetchedTotalBytes.Load()
|
||||||
|
// start downloading
|
||||||
|
eg, gctx = errgroup.WithContext(ctx)
|
||||||
|
eg.SetLimit(config.C().Workers)
|
||||||
|
for _, file := range t.files {
|
||||||
|
eg.Go(func() error {
|
||||||
|
t.processingMu.RLock()
|
||||||
|
if _, ok := t.processing[file.URL]; ok {
|
||||||
|
return fmt.Errorf("file %s is already being processed", file.URL)
|
||||||
|
}
|
||||||
|
t.processingMu.RUnlock()
|
||||||
|
t.processingMu.Lock()
|
||||||
|
t.processing[file.URL] = file
|
||||||
|
t.processingMu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
t.processingMu.Lock()
|
||||||
|
delete(t.processing, file.URL)
|
||||||
|
t.processingMu.Unlock()
|
||||||
|
}()
|
||||||
|
err := t.processLink(gctx, file)
|
||||||
|
t.downloaded.Add(1)
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
logger.Debug("Link processing canceled")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error processing link %s: %v", file.URL, err)
|
||||||
|
return fmt.Errorf("failed to process link %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = eg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error during directlinks task execution: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Infof("Directlinks task %s completed successfully", t.ID)
|
||||||
|
}
|
||||||
|
if t.Progress != nil {
|
||||||
|
t.Progress.OnDone(ctx, t, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) processLink(ctx context.Context, file *File) error {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
err := retry.Retry(func() error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, file.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create GET request for %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
resp, err := t.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to GET %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("GET %s returned status %d", file.URL, resp.StatusCode)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, ctxkey.ContentLength, file.Size)
|
||||||
|
if t.stream {
|
||||||
|
return t.Storage.Save(ctx, resp.Body, filepath.Join(t.StorPath, file.Name))
|
||||||
|
}
|
||||||
|
cacheFile, err := fsutil.CreateFile(filepath.Join(config.C().Temp.BasePath,
|
||||||
|
fmt.Sprintf("direct_%s_%s", t.ID, file.Name)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := cacheFile.CloseAndRemove(); err != nil {
|
||||||
|
logger.Errorf("Failed to close and remove cache file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wr := ioutil.NewProgressWriter(cacheFile, func(n int) {
|
||||||
|
t.downloadedBytes.Add(int64(n))
|
||||||
|
if t.Progress != nil {
|
||||||
|
t.Progress.OnProgress(ctx, t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
copyResultCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(wr, resp.Body)
|
||||||
|
copyResultCh <- err
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-copyResultCh:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file %s to cache file: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
_, err = cacheFile.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to seek cache file for resource %s: %w", file.URL, err)
|
||||||
|
}
|
||||||
|
return t.Storage.Save(ctx, cacheFile, filepath.Join(t.StorPath, file.Name))
|
||||||
|
}, retry.RetryTimes(uint(config.C().Retry)), retry.Context(ctx))
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
196
core/tasks/directlinks/progress.go
Normal file
196
core/tasks/directlinks/progress.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package directlinks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskInfo interface {
|
||||||
|
TotalBytes() int64
|
||||||
|
TotalFiles() int
|
||||||
|
TaskID() string
|
||||||
|
StorageName() string
|
||||||
|
StoragePath() string
|
||||||
|
DownloadedBytes() int64
|
||||||
|
Processing() []FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileInfo interface {
|
||||||
|
FileName() string
|
||||||
|
FileSize() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressTracker interface {
|
||||||
|
OnStart(ctx context.Context, info TaskInfo)
|
||||||
|
OnProgress(ctx context.Context, info TaskInfo)
|
||||||
|
OnDone(ctx context.Context, info TaskInfo, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Progress struct {
|
||||||
|
msgID int
|
||||||
|
chatID int64
|
||||||
|
start time.Time
|
||||||
|
lastUpdatePercent atomic.Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDone implements ProgressTracker.
|
||||||
|
func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
logger.Infof("Parsed task %s was canceled", info.TaskID())
|
||||||
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
|
if ext != nil {
|
||||||
|
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: p.msgID,
|
||||||
|
Message: fmt.Sprintf("处理已取消: %s", info.TaskID()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Errorf("Parsed task %s failed: %s", info.TaskID(), err)
|
||||||
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
|
if ext != nil {
|
||||||
|
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: p.msgID,
|
||||||
|
Message: fmt.Sprintf("处理失败: %s", err.Error()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Infof("Parsed task %s completed successfully", info.TaskID())
|
||||||
|
|
||||||
|
entityBuilder := entity.Builder{}
|
||||||
|
if err := styling.Perform(&entityBuilder,
|
||||||
|
styling.Plain("处理完成, 文件数量: "),
|
||||||
|
styling.Code(fmt.Sprintf("%d", info.TotalFiles())),
|
||||||
|
styling.Plain("\n保存路径: "),
|
||||||
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
|
); err != nil {
|
||||||
|
logger.Errorf("Failed to build entities: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, entities := entityBuilder.Complete()
|
||||||
|
req := &tg.MessagesEditMessageRequest{
|
||||||
|
ID: p.msgID,
|
||||||
|
}
|
||||||
|
req.SetMessage(text)
|
||||||
|
req.SetEntities(entities)
|
||||||
|
|
||||||
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
|
if ext != nil {
|
||||||
|
ext.EditMessage(p.chatID, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnProgress implements ProgressTracker.
|
||||||
|
func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
|
||||||
|
if !shouldUpdateProgress(info.TotalBytes(), info.DownloadedBytes(), int(p.lastUpdatePercent.Load())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
percent := int((info.DownloadedBytes() * 100) / info.TotalBytes())
|
||||||
|
if p.lastUpdatePercent.Load() == int32(percent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.lastUpdatePercent.Store(int32(percent))
|
||||||
|
log.FromContext(ctx).Debugf("Progress update: %s, %d/%d", info.TaskID(), info.DownloadedBytes(), info.TotalBytes())
|
||||||
|
entityBuilder := entity.Builder{}
|
||||||
|
var entities []tg.MessageEntityClass
|
||||||
|
if err := styling.Perform(&entityBuilder,
|
||||||
|
styling.Plain("正在下载\n总大小: "),
|
||||||
|
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalFiles())),
|
||||||
|
styling.Plain("\n正在处理:\n"),
|
||||||
|
func() styling.StyledTextOption {
|
||||||
|
var lines []string
|
||||||
|
for _, elem := range info.Processing() {
|
||||||
|
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = append(lines, " - 无")
|
||||||
|
}
|
||||||
|
return styling.Plain(slice.Join(lines, "\n"))
|
||||||
|
}(),
|
||||||
|
styling.Plain("\n平均速度: "),
|
||||||
|
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.DownloadedBytes(), p.start)/(1024*1024))),
|
||||||
|
styling.Plain("\n当前进度: "),
|
||||||
|
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.DownloadedBytes())/float64(info.TotalBytes())*100)),
|
||||||
|
); err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, entities := entityBuilder.Complete()
|
||||||
|
req := &tg.MessagesEditMessageRequest{
|
||||||
|
ID: p.msgID,
|
||||||
|
}
|
||||||
|
req.SetMessage(text)
|
||||||
|
req.SetEntities(entities)
|
||||||
|
req.SetReplyMarkup(&tg.ReplyInlineMarkup{
|
||||||
|
Rows: []tg.KeyboardButtonRow{
|
||||||
|
{
|
||||||
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
|
tgutil.BuildCancelButton(info.TaskID()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
|
if ext != nil {
|
||||||
|
ext.EditMessage(p.chatID, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnStart implements ProgressTracker.
|
||||||
|
func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
p.start = time.Now()
|
||||||
|
p.lastUpdatePercent.Store(0)
|
||||||
|
logger.Infof("Direct links task started: message_id=%d, chat_id=%d", p.msgID, p.chatID)
|
||||||
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
|
if ext == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entityBuilder := entity.Builder{}
|
||||||
|
var entities []tg.MessageEntityClass
|
||||||
|
if err := styling.Perform(&entityBuilder,
|
||||||
|
styling.Plain(fmt.Sprintf("开始下载, 总大小: %.2f MB (%d 个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalFiles()))); err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, entities := entityBuilder.Complete()
|
||||||
|
req := &tg.MessagesEditMessageRequest{
|
||||||
|
ID: p.msgID,
|
||||||
|
}
|
||||||
|
req.SetMessage(text)
|
||||||
|
req.SetEntities(entities)
|
||||||
|
req.SetReplyMarkup(&tg.ReplyInlineMarkup{
|
||||||
|
Rows: []tg.KeyboardButtonRow{
|
||||||
|
{
|
||||||
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
|
tgutil.BuildCancelButton(info.TaskID()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
ext.EditMessage(p.chatID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ProgressTracker = (*Progress)(nil)
|
||||||
|
|
||||||
|
func NewProgress(msgID int, userID int64) ProgressTracker {
|
||||||
|
return &Progress{
|
||||||
|
msgID: msgID,
|
||||||
|
chatID: userID,
|
||||||
|
}
|
||||||
|
}
|
||||||
130
core/tasks/directlinks/task.go
Normal file
130
core/tasks/directlinks/task.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package directlinks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) FileName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) FileSize() int64 {
|
||||||
|
return f.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ core.Executable = (*Task)(nil)
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID string
|
||||||
|
ctx context.Context
|
||||||
|
files []*File
|
||||||
|
Storage storage.Storage
|
||||||
|
StorPath string
|
||||||
|
Progress ProgressTracker
|
||||||
|
|
||||||
|
client *http.Client // [TODO] parallel download
|
||||||
|
stream bool
|
||||||
|
totalBytes int64 // total bytes to download
|
||||||
|
downloadedBytes atomic.Int64 // downloaded bytes
|
||||||
|
totalFiles int64 // total files to download
|
||||||
|
downloaded atomic.Int64 // downloaded files count
|
||||||
|
processing map[string]*File // {"url": File}
|
||||||
|
processingMu sync.RWMutex
|
||||||
|
failed map[string]error // [TODO] errors for each file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title implements core.Exectable.
|
||||||
|
func (t *Task) Title() string {
|
||||||
|
return fmt.Sprintf("[%s](%s...->%s:%s)", t.Type(), t.files[0].Name, t.Storage.Name(), t.StorPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadedBytes implements TaskInfo.
|
||||||
|
func (t *Task) DownloadedBytes() int64 {
|
||||||
|
return t.downloadedBytes.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing implements TaskInfo.
|
||||||
|
func (t *Task) Processing() []FileInfo {
|
||||||
|
t.processingMu.RLock()
|
||||||
|
defer t.processingMu.RUnlock()
|
||||||
|
infos := make([]FileInfo, 0, len(t.processing))
|
||||||
|
for _, f := range t.processing {
|
||||||
|
infos = append(infos, f)
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageName implements TaskInfo.
|
||||||
|
func (t *Task) StorageName() string {
|
||||||
|
return t.Storage.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoragePath implements TaskInfo.
|
||||||
|
func (t *Task) StoragePath() string {
|
||||||
|
return t.StorPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalBytes implements TaskInfo.
|
||||||
|
func (t *Task) TotalBytes() int64 {
|
||||||
|
return t.totalBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalFiles implements TaskInfo.
|
||||||
|
func (t *Task) TotalFiles() int {
|
||||||
|
return int(t.totalFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) Type() tasktype.TaskType {
|
||||||
|
return tasktype.TaskTypeDirectlinks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) TaskID() string {
|
||||||
|
return t.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTask(
|
||||||
|
id string,
|
||||||
|
ctx context.Context,
|
||||||
|
links []string,
|
||||||
|
stor storage.Storage,
|
||||||
|
storPath string,
|
||||||
|
progressTracker ProgressTracker,
|
||||||
|
) *Task {
|
||||||
|
_, ok := stor.(storage.StorageCannotStream)
|
||||||
|
stream := config.C().Stream && !ok
|
||||||
|
files := make([]*File, 0, len(links))
|
||||||
|
for _, link := range links {
|
||||||
|
files = append(files, &File{
|
||||||
|
URL: link,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &Task{
|
||||||
|
ID: id,
|
||||||
|
ctx: ctx,
|
||||||
|
files: files,
|
||||||
|
Storage: stor,
|
||||||
|
StorPath: storPath,
|
||||||
|
Progress: progressTracker,
|
||||||
|
stream: stream,
|
||||||
|
client: http.DefaultClient,
|
||||||
|
processing: make(map[string]*File),
|
||||||
|
processingMu: sync.RWMutex{},
|
||||||
|
failed: make(map[string]error),
|
||||||
|
totalFiles: int64(len(files)),
|
||||||
|
}
|
||||||
|
}
|
||||||
205
core/tasks/directlinks/util.go
Normal file
205
core/tasks/directlinks/util.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package directlinks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseFilename extracts filename from Content-Disposition header
|
||||||
|
// It handles multiple encoding scenarios:
|
||||||
|
// 1. RFC 5987/RFC 2231 format: filename*=UTF-8”%E6%B5%8B%E8%AF%95.zip (preferred, checked first)
|
||||||
|
// 2. MIME encoded-word: filename="=?UTF-8?B?5rWL6K+VLnppcA==?="
|
||||||
|
// 3. URL-encoded: filename="%E6%B5%8B%E8%AF%95.zip"
|
||||||
|
// 4. Plain ASCII filename
|
||||||
|
//
|
||||||
|
// The key fix is checking filename*= first before mime.ParseMediaType, because
|
||||||
|
// some servers send Content-Disposition headers with invalid characters that cause
|
||||||
|
// mime.ParseMediaType to fail, but the filename*= parameter is still valid.
|
||||||
|
func parseFilename(contentDisposition string) string {
|
||||||
|
// First, try to find filename*= (RFC 5987 format, most reliable for non-ASCII)
|
||||||
|
if filename := parseFilenameExtended(contentDisposition); filename != "" {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try standard MIME parsing for regular filename= parameter
|
||||||
|
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||||
|
if err == nil {
|
||||||
|
if filename := params["filename"]; filename != "" {
|
||||||
|
return decodeFilenameParam(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: manual parsing if mime.ParseMediaType fails
|
||||||
|
return parseFilenameFallback(contentDisposition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilenameExtended parses RFC 5987/RFC 2231 extended parameter format
|
||||||
|
// Format: filename*=charset'language'value (e.g., UTF-8”%E6%B5%8B%E8%AF%95.zip)
|
||||||
|
func parseFilenameExtended(cd string) string {
|
||||||
|
// Look for filename*= (case-insensitive)
|
||||||
|
lower := strings.ToLower(cd)
|
||||||
|
idx := strings.Index(lower, "filename*=")
|
||||||
|
if idx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the value after filename*=
|
||||||
|
value := cd[idx+len("filename*="):]
|
||||||
|
|
||||||
|
// Find the end of the value (next ; or end of string)
|
||||||
|
if endIdx := strings.Index(value, ";"); endIdx != -1 {
|
||||||
|
value = value[:endIdx]
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
// Parse charset'language'encoded-value format
|
||||||
|
// Common format: UTF-8''%E6%B5%8B%E8%AF%95.zip
|
||||||
|
parts := strings.SplitN(value, "''", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
// parts[0] is charset (e.g., "UTF-8")
|
||||||
|
// parts[1] is percent-encoded value
|
||||||
|
decoded, err := url.QueryUnescape(parts[1])
|
||||||
|
if err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try with single quote delimiter as well (some servers use this)
|
||||||
|
parts = strings.SplitN(value, "'", 3)
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
decoded, err := url.QueryUnescape(parts[2])
|
||||||
|
if err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
|
||||||
|
//
|
||||||
|
// If unescaping fails, it returns the original string.
|
||||||
|
func tryUrlQueryUnescape(s string) string {
|
||||||
|
if decoded, err := url.QueryUnescape(s); err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeFilenameParam decodes a filename parameter value
|
||||||
|
// Handles MIME encoded-word, URL encoding, and GBK encoding fallback
|
||||||
|
func decodeFilenameParam(filename string) string {
|
||||||
|
// Check if the filename is MIME encoded-word (e.g., =?UTF-8?B?...?=)
|
||||||
|
if strings.HasPrefix(filename, "=?") {
|
||||||
|
decoder := new(mime.WordDecoder)
|
||||||
|
// Some servers use "UTF8" instead of "UTF-8", create a normalized copy
|
||||||
|
normalizedFilename := strings.Replace(filename, "UTF8", "UTF-8", 1)
|
||||||
|
if decoded, err := decoder.Decode(normalizedFilename); err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try URL decoding
|
||||||
|
decoded := tryUrlQueryUnescape(filename)
|
||||||
|
|
||||||
|
// Check if the result is valid UTF-8. If not, try GBK decoding.
|
||||||
|
// This handles the case where Chinese Windows servers send GBK-encoded filenames
|
||||||
|
// which appear as garbled characters (e.g., "下载地址.zip" -> "<22><><EFBFBD>ص<EFBFBD>ַ.zip")
|
||||||
|
if !utf8.ValidString(decoded) {
|
||||||
|
if gbkDecoded := tryDecodeGBK(decoded); gbkDecoded != "" {
|
||||||
|
return gbkDecoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbkDecoder is a reusable GBK decoder for better performance
|
||||||
|
var gbkDecoder = simplifiedchinese.GBK.NewDecoder()
|
||||||
|
|
||||||
|
// tryDecodeGBK attempts to decode a string as GBK/GB2312/GB18030 encoding
|
||||||
|
// Returns empty string if decoding fails or result is not valid UTF-8
|
||||||
|
func tryDecodeGBK(s string) string {
|
||||||
|
// GBK uses 1-2 bytes per character. Single-byte chars are 0x00-0x7F (ASCII compatible).
|
||||||
|
// Double-byte chars have first byte 0x81-0xFE and second byte 0x40-0xFE.
|
||||||
|
// Skip if string is empty or all ASCII (valid UTF-8)
|
||||||
|
if len(s) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a fresh decoder since the transform state may be corrupted
|
||||||
|
decoder := gbkDecoder
|
||||||
|
decoded, err := decoder.Bytes([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
result := string(decoded)
|
||||||
|
if utf8.ValidString(result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilenameFallback manually parses filename= when mime.ParseMediaType fails
|
||||||
|
func parseFilenameFallback(cd string) string {
|
||||||
|
// Look for filename= (case-insensitive)
|
||||||
|
lower := strings.ToLower(cd)
|
||||||
|
idx := strings.Index(lower, "filename=")
|
||||||
|
if idx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip "filename=" prefix
|
||||||
|
value := cd[idx+len("filename="):]
|
||||||
|
|
||||||
|
// Find the end of the value
|
||||||
|
if endIdx := strings.Index(value, ";"); endIdx != -1 {
|
||||||
|
value = value[:endIdx]
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
// Remove quotes if present
|
||||||
|
if len(value) >= 2 {
|
||||||
|
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||||
|
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeFilenameParam(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressUpdatesLevels = []struct {
|
||||||
|
size int64 // 文件大小阈值
|
||||||
|
stepPercent int // 每多少 % 更新一次
|
||||||
|
}{
|
||||||
|
{10 << 20, 100},
|
||||||
|
{50 << 20, 50},
|
||||||
|
{200 << 20, 20},
|
||||||
|
{500 << 20, 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUpdateProgress(total, downloaded int64, lastUpdatePercent int) bool {
|
||||||
|
if total <= 0 || downloaded <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
percent := int((downloaded * 100) / total)
|
||||||
|
if percent <= lastUpdatePercent {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
step := progressUpdatesLevels[len(progressUpdatesLevels)-1].stepPercent
|
||||||
|
for _, lvl := range progressUpdatesLevels {
|
||||||
|
if total < lvl.size {
|
||||||
|
step = lvl.stepPercent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return percent >= lastUpdatePercent+step
|
||||||
|
}
|
||||||
@@ -2,24 +2,28 @@ package parsed
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ core.Executable = (*Task)(nil)
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string
|
ID string
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Stor storage.Storage
|
Stor storage.Storage
|
||||||
StorPath string
|
StorPath string
|
||||||
item *parser.Item
|
item *parser.Item
|
||||||
httpClient *http.Client
|
httpClient *http.Client // [TODO] btorrent support?
|
||||||
progress ProgressTracker
|
progress ProgressTracker
|
||||||
stream bool
|
stream bool
|
||||||
|
|
||||||
@@ -32,6 +36,11 @@ type Task struct {
|
|||||||
failed map[string]error // [TODO] errors for each resource
|
failed map[string]error // [TODO] errors for each resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title implements core.Exectable.
|
||||||
|
func (t *Task) Title() string {
|
||||||
|
return fmt.Sprintf("[%s](%s->%s:%s)", t.Type(), t.item.Title, t.Stor.Name(), t.StorPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) Type() tasktype.TaskType {
|
func (t *Task) Type() tasktype.TaskType {
|
||||||
return tasktype.TaskTypeParseditem
|
return tasktype.TaskTypeParseditem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/duke-git/lancet/v2/retry"
|
"github.com/duke-git/lancet/v2/retry"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"go.uber.org/multierr"
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,13 +47,10 @@ func (t *Task) processPic(ctx context.Context, picUrl string, index int) error {
|
|||||||
retry.Context(ctx),
|
retry.Context(ctx),
|
||||||
retry.RetryTimes(uint(config.C().Retry)),
|
retry.RetryTimes(uint(config.C().Retry)),
|
||||||
}
|
}
|
||||||
var lastErr error
|
|
||||||
err := retry.Retry(func() error {
|
err := retry.Retry(func() error {
|
||||||
var body io.ReadCloser
|
body, err := t.client.Download(ctx, picUrl)
|
||||||
body, lastErr = t.client.Download(ctx, picUrl)
|
if err != nil {
|
||||||
if lastErr != nil {
|
return fmt.Errorf("failed to download picture %s: %w", picUrl, err)
|
||||||
lastErr = fmt.Errorf("failed to download picture %s: %w", picUrl, lastErr)
|
|
||||||
return lastErr
|
|
||||||
}
|
}
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
filename := fmt.Sprintf("%d%s", index+1, path.Ext(picUrl))
|
filename := fmt.Sprintf("%d%s", index+1, path.Ext(picUrl))
|
||||||
@@ -63,8 +59,7 @@ func (t *Task) processPic(ctx context.Context, picUrl string, index int) error {
|
|||||||
fmt.Sprintf("tph_%s_%s", t.TaskID(), filename),
|
fmt.Sprintf("tph_%s_%s", t.TaskID(), filename),
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("failed to create cache file for picture %s: %w", filename, err)
|
return fmt.Errorf("failed to create cache file for picture %s: %w", filename, err)
|
||||||
return lastErr
|
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := cacheFile.CloseAndRemove(); err != nil {
|
if err := cacheFile.CloseAndRemove(); err != nil {
|
||||||
@@ -72,26 +67,26 @@ func (t *Task) processPic(ctx context.Context, picUrl string, index int) error {
|
|||||||
logger.Errorf("Failed to close and remove cache file for picture %s: %v", filename, err)
|
logger.Errorf("Failed to close and remove cache file for picture %s: %v", filename, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
_, lastErr = io.Copy(cacheFile, body)
|
_, err = io.Copy(cacheFile, body)
|
||||||
if lastErr != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("failed to copy picture %s to cache file: %w", filename, lastErr)
|
return fmt.Errorf("failed to copy picture %s to cache file: %w", filename, err)
|
||||||
return lastErr
|
|
||||||
}
|
}
|
||||||
_, err = cacheFile.Seek(0, 0)
|
_, err = cacheFile.Seek(0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("failed to seek cache file for picture %s: %w", filename, err)
|
return fmt.Errorf("failed to seek cache file for picture %s: %w", filename, err)
|
||||||
return lastErr
|
}
|
||||||
|
err = t.Stor.Save(ctx, cacheFile, path.Join(t.StorPath, filename))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save picture %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
lastErr = t.Stor.Save(ctx, cacheFile, path.Join(t.StorPath, filename))
|
|
||||||
} else {
|
} else {
|
||||||
lastErr = t.Stor.Save(ctx, body, path.Join(t.StorPath, filename))
|
err = t.Stor.Save(ctx, body, path.Join(t.StorPath, filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastErr != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("failed to save picture %s: %w", filename, lastErr)
|
return fmt.Errorf("failed to save picture %s: %w", filename, err)
|
||||||
return lastErr
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, retryOpts...)
|
}, retryOpts...)
|
||||||
return multierr.Combine(err, lastErr)
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package telegraph
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/telegraph"
|
"github.com/krau/SaveAny-Bot/pkg/telegraph"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ core.Executable = (*Task)(nil)
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string
|
ID string
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
@@ -24,6 +28,11 @@ type Task struct {
|
|||||||
downloaded atomic.Int64
|
downloaded atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title implements core.Exectable.
|
||||||
|
func (t *Task) Title() string {
|
||||||
|
return fmt.Sprintf("[%s](%s->%s:%s)", t.Type(), t.PhPath, t.Stor.Name(), t.StorPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) Type() tasktype.TaskType {
|
func (t *Task) Type() tasktype.TaskType {
|
||||||
return tasktype.TaskTypeTphpics
|
return tasktype.TaskTypeTphpics
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/duke-git/lancet/v2/retry"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *Task) Execute(ctx context.Context) error {
|
func (t *Task) Execute(ctx context.Context) error {
|
||||||
@@ -40,7 +40,7 @@ func (t *Task) Execute(ctx context.Context) error {
|
|||||||
t.Progress.OnDone(ctx, t, err)
|
t.Progress.OnDone(ctx, t, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
_, err = tfile.NewDownloader(t.File).Parallel(ctx, wrAt)
|
_, err = tdler.NewDownloader(t.File).Parallel(ctx, wrAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download file: %w", err)
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -57,30 +57,19 @@ func (t *Task) Execute(ctx context.Context) error {
|
|||||||
return fmt.Errorf("failed to get file stat: %w", err)
|
return fmt.Errorf("failed to get file stat: %w", err)
|
||||||
}
|
}
|
||||||
vctx := context.WithValue(ctx, ctxkey.ContentLength, fileStat.Size())
|
vctx := context.WithValue(ctx, ctxkey.ContentLength, fileStat.Size())
|
||||||
for i := range config.C().Retry + 1 {
|
err = retry.Retry(func() error {
|
||||||
if err = vctx.Err(); err != nil {
|
file, err := os.Open(t.localPath)
|
||||||
return fmt.Errorf("context canceled while saving file: %w", err)
|
|
||||||
}
|
|
||||||
var file *os.File
|
|
||||||
file, err = os.Open(t.localPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open cache file: %w", err)
|
return fmt.Errorf("failed to open cache file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err = t.Storage.Save(vctx, file, t.Path); err != nil {
|
if err = t.Storage.Save(vctx, file, t.Path); err != nil {
|
||||||
if i == config.C().Retry {
|
return fmt.Errorf("failed to save file: %w", err)
|
||||||
return fmt.Errorf("failed to save file: %w", err)
|
|
||||||
}
|
|
||||||
logger.Errorf("Failed to save file: %s, retrying...", err)
|
|
||||||
select {
|
|
||||||
case <-vctx.Done():
|
|
||||||
return fmt.Errorf("context canceled during retry delay: %w", vctx.Err())
|
|
||||||
case <-time.After(time.Duration(i*500) * time.Millisecond):
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
}, retry.RetryTimes(uint(config.C().Retry)), retry.Context(vctx))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save file after retries: %w", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to save file after retries")
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ func executeStream(ctx context.Context, task *Task) error {
|
|||||||
errg.Go(func() error {
|
errg.Go(func() error {
|
||||||
defer pw.Close()
|
defer pw.Close()
|
||||||
logger.Info("Starting file download in stream mode")
|
logger.Info("Starting file download in stream mode")
|
||||||
_, err := tfile.NewDownloader(task.File).Stream(uploadCtx, wr)
|
_, err := tdler.NewDownloader(task.File).Stream(uploadCtx, wr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to download file: %v", err)
|
logger.Errorf("Failed to download file: %v", err)
|
||||||
pw.CloseWithError(err)
|
pw.CloseWithError(err)
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ core.Executable = (*Task)(nil)
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string
|
ID string
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
@@ -22,6 +25,11 @@ type Task struct {
|
|||||||
localPath string
|
localPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title implements core.Exectable.
|
||||||
|
func (t *Task) Title() string {
|
||||||
|
return fmt.Sprintf("[%s](%s->%s:%s)", t.Type(), t.File.Name(), t.Storage.Name(), t.Path)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) Type() tasktype.TaskType {
|
func (t *Task) Type() tasktype.TaskType {
|
||||||
return tasktype.TaskTypeTgfiles
|
return tasktype.TaskTypeTgfiles
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
_ "github.com/ncruces/go-sqlite3/embed"
|
|
||||||
"github.com/ncruces/go-sqlite3/gormlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
glogger "gorm.io/gorm/logger"
|
glogger "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
@@ -23,7 +21,7 @@ func Init(ctx context.Context) {
|
|||||||
logger.Fatal("Failed to create data directory: ", err)
|
logger.Fatal("Failed to create data directory: ", err)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
db, err = gorm.Open(gormlite.Open(config.C().DB.Path), &gorm.Config{
|
db, err = gorm.Open(GetDialect(config.C().DB.Path), &gorm.Config{
|
||||||
Logger: glogger.New(logger, glogger.Config{
|
Logger: glogger.New(logger, glogger.Config{
|
||||||
Colorful: true,
|
Colorful: true,
|
||||||
SlowThreshold: time.Second * 5,
|
SlowThreshold: time.Second * 5,
|
||||||
|
|||||||
13
database/driver.go
Normal file
13
database/driver.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !sqlite_glebarez
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/ncruces/go-sqlite3/embed"
|
||||||
|
"github.com/ncruces/go-sqlite3/gormlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDialect(dsn string) gorm.Dialector {
|
||||||
|
return gormlite.Open(dsn)
|
||||||
|
}
|
||||||
12
database/driver_glebarez.go
Normal file
12
database/driver_glebarez.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build sqlite_glebarez
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDialect(dsn string) gorm.Dialector {
|
||||||
|
return sqlite.Open(dsn)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ type User struct {
|
|||||||
ChatID int64 `gorm:"uniqueIndex;not null"`
|
ChatID int64 `gorm:"uniqueIndex;not null"`
|
||||||
Silent bool
|
Silent bool
|
||||||
DefaultStorage string
|
DefaultStorage string
|
||||||
|
DefaultDir uint // Dir.ID
|
||||||
Dirs []Dir
|
Dirs []Dir
|
||||||
ApplyRule bool
|
ApplyRule bool
|
||||||
Rules []Rule
|
Rules []Rule
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Save Any Bot is a tool that allows you to save files from Telegram to various st
|
|||||||
- Automatic organization based on storage rules
|
- Automatic organization based on storage rules
|
||||||
- Supports multiple storage backends:
|
- Supports multiple storage backends:
|
||||||
- Alist
|
- Alist
|
||||||
- Minio (S3 compatible)
|
- S3
|
||||||
- WebDAV
|
- WebDAV
|
||||||
- Telegram (re-upload to specified chat)
|
- Telegram (re-upload to specified chat)
|
||||||
- Local disk
|
- Local disk
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ Each storage endpoint requires at least the following fields:
|
|||||||
- `local`: Local disk
|
- `local`: Local disk
|
||||||
- `alist`: Alist
|
- `alist`: Alist
|
||||||
- `webdav`: WebDAV
|
- `webdav`: WebDAV
|
||||||
- `minio`: MinIO (compatible with S3 API)
|
- `s3`: aws S3 and other S3 compatible services
|
||||||
- `telegram`: Upload to Telegram
|
- `telegram`: Upload to Telegram
|
||||||
|
|
||||||
Example, this is a configuration that includes local storage and webdav storage:
|
Example, this is a configuration that includes local storage and webdav storage:
|
||||||
|
|||||||
@@ -41,17 +41,18 @@ password = "your_password" # Password for WebDAV
|
|||||||
base_path = "/path/to/webdav" # Base path in WebDAV, all files will be stored under this path
|
base_path = "/path/to/webdav" # Base path in WebDAV, all files will be stored under this path
|
||||||
```
|
```
|
||||||
|
|
||||||
## MinIO (S3)
|
## S3
|
||||||
|
|
||||||
`type=minio`
|
`type=s3`
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
endpoint = "minio.example.com" # Endpoint for MinIO or S3
|
endpoint = "s3.example.com" # Endpoint for S3
|
||||||
access_key_id = "your_access_key_id" # Access key ID for MinIO or S3
|
region = "us-east-1" # Region for S3
|
||||||
secret_access_key = "your_secret_access_key" # Secret access key for MinIO or S3
|
access_key_id = "your_access_key_id" # Access key ID for S3
|
||||||
bucket_name = "your_bucket_name" # Bucket name for MinIO or S3
|
secret_access_key = "your_secret_access_key" # Secret access key for S3
|
||||||
|
bucket_name = "your_bucket_name" # Bucket name for S3
|
||||||
use_ssl = true # Whether to use SSL, default is true
|
use_ssl = true # Whether to use SSL, default is true
|
||||||
base_path = "/path/to/minio" # Base path in MinIO, all files will be stored under this path
|
base_path = "/path/to/s3" # Base path in S3, all files will be stored under this path
|
||||||
```
|
```
|
||||||
|
|
||||||
## Telegram
|
## Telegram
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ title: 介绍
|
|||||||
- 使用 js 编写解析器插件以转存任意网站的文件
|
- 使用 js 编写解析器插件以转存任意网站的文件
|
||||||
- 存储端支持:
|
- 存储端支持:
|
||||||
- Alist
|
- Alist
|
||||||
- S3 (MinioSDK)
|
- S3
|
||||||
- WebDAV
|
- WebDAV
|
||||||
- 本地磁盘
|
- 本地磁盘
|
||||||
- Telegram (重传回指定聊天)
|
- Telegram (重传回指定聊天)
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ Stream 模式对于磁盘空间有限的部署环境十分有用, 但也有一
|
|||||||
- `workers`: 同时处理任务数量, 默认为 3
|
- `workers`: 同时处理任务数量, 默认为 3
|
||||||
- `threads`: 下载文件时使用的线程数, 默认为 4. 仅在未启用 Stream 模式时生效.
|
- `threads`: 下载文件时使用的线程数, 默认为 4. 仅在未启用 Stream 模式时生效.
|
||||||
- `retry`: 任务失败时的重试次数, 默认为 3.
|
- `retry`: 任务失败时的重试次数, 默认为 3.
|
||||||
|
- `proxy`: 全局代理配置, 配置后程序内一切网络连接将会尝试使用该代理, 可选.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
stream = false
|
||||||
|
workers = 3
|
||||||
|
threads = 4
|
||||||
|
retry = 3
|
||||||
|
proxy = "socks5://127.0.0.1:7890"
|
||||||
|
```
|
||||||
|
|
||||||
### Telegram 配置
|
### Telegram 配置
|
||||||
|
|
||||||
@@ -93,7 +102,7 @@ session = "data/usersession.db"
|
|||||||
- `local`: 本地磁盘
|
- `local`: 本地磁盘
|
||||||
- `alist`: Alist
|
- `alist`: Alist
|
||||||
- `webdav`: WebDAV
|
- `webdav`: WebDAV
|
||||||
- `minio`: MinIO (兼容 S3 API)
|
- `s3`: aws S3 及其他兼容 S3 的服务
|
||||||
- `telegram`: 上传到 Telegram
|
- `telegram`: 上传到 Telegram
|
||||||
|
|
||||||
示例, 这是一个包含本地存储和 webdav 存储的配置:
|
示例, 这是一个包含本地存储和 webdav 存储的配置:
|
||||||
|
|||||||
@@ -41,19 +41,34 @@ password = "your_password" # WebDAV 的密码
|
|||||||
base_path = "/path/to/webdav" # WebDAV 中的基础路径, 所有文件将存储在此路径下
|
base_path = "/path/to/webdav" # WebDAV 中的基础路径, 所有文件将存储在此路径下
|
||||||
```
|
```
|
||||||
|
|
||||||
## MinIO (S3)
|
## S3
|
||||||
|
|
||||||
`type=minio`
|
`type=s3`
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
endpoint = "minio.example.com" # MinIO 或 S3 的端点
|
endpoint = "s3.example.com" # S3 的端点, 默认为 aws S3 的端点
|
||||||
access_key_id = "your_access_key_id" # MinIO 或 S3 的访问密钥 ID
|
region = "us-east-1" # S3 的区域
|
||||||
secret_access_key = "your_secret_access_key" # MinIO 或 S3 的秘密访问密钥
|
access_key_id = "your_access_key_id" # S3 的访问密钥 ID
|
||||||
bucket_name = "your_bucket_name" # MinIO 或 S3 的存储桶名称
|
secret_access_key = "your_secret_access_key" # S3 的秘密访问密钥
|
||||||
use_ssl = true # 是否使用 SSL, 默认为 true
|
bucket_name = "your_bucket_name" # S3 的存储桶名称
|
||||||
base_path = "/path/to/minio" # MinIO 中的基础路径, 所有文件将存储在此路径下
|
base_path = "/path/to/s3" # S3 中的基础路径, 所有文件将存储在此路径下
|
||||||
|
virtual_host = false # 使用虚拟主机风格的 URL, 默认为 false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
虚拟主机风格的 URL 示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://your_bucket_name.s3.example.com/path/to/s3/your_file
|
||||||
|
```
|
||||||
|
|
||||||
|
路径风格(关闭 virtual_host)的 URL 示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://s3.example.com/your_bucket_name/path/to/s3/your_file
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你使用的是第三方的兼容 S3 的服务, 一般使用的是路径风格的 URL. 而 AWS S3 则通常使用虚拟主机风格的 URL. 详情请参考你所使用的 S3 兼容服务的文档.
|
||||||
|
|
||||||
## Telegram
|
## Telegram
|
||||||
|
|
||||||
`type=telegram`
|
`type=telegram`
|
||||||
@@ -62,4 +77,7 @@ base_path = "/path/to/minio" # MinIO 中的基础路径, 所有文件将存储
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
chat_id = "123456789" # Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
chat_id = "123456789" # Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
||||||
|
force_file = false # 是否强制使用文件方式发送, 默认为 false.
|
||||||
|
skip_large = true # 是否跳过大文件, 默认为 true. 如果启用, 超过 Telegram 限制的文件将不会上传.
|
||||||
|
spilt_size_mb = 2000 # 分卷大小, 单位 MB, 默认为 2000 MB (2 GB). 超过该大小的文件将被分割成多个部分上传.(使用 zip 格式)
|
||||||
```
|
```
|
||||||
@@ -129,17 +129,39 @@ docker run -d --name saveany-bot \
|
|||||||
ghcr.io/krau/saveany-bot:latest
|
ghcr.io/krau/saveany-bot:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{{< hint info >}}
|
||||||
|
关于 docker 镜像的变体版本
|
||||||
|
<br />
|
||||||
|
<ul>
|
||||||
|
<li>默认版本: 包含所有功能和依赖, 体积较大. 如果没有特殊需要, 请使用此版本</li>
|
||||||
|
<li>micro: 精简版本, 去除部分可选依赖, 体积较小</li>
|
||||||
|
<li>pico: 极简版本, 仅包含核心功能, 体积最小</li>
|
||||||
|
</ul>
|
||||||
|
你可以根据需要, 通过指定不同的标签来拉取合适的版本, 例如: <code>ghcr.io/krau/saveany-bot:micro</code>
|
||||||
|
<br />
|
||||||
|
关于变体版本的更详细的区别, 请参考项目根目录下的 Dockerfile 文件.
|
||||||
|
{{< /hint >}}
|
||||||
|
|
||||||
## 更新
|
## 更新
|
||||||
|
|
||||||
向 Bot 发送 `/update` 指令检查更新并升级, 或者使用 CLI 命令更新:
|
若使用预编译二进制文件部署, 使用以下 CLI 命令更新:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./saveany-bot up
|
./saveany-bot up
|
||||||
```
|
```
|
||||||
|
|
||||||
如果是 Docker 部署, 还可以使用以下命令更新:
|
如果是 Docker 部署, 使用以下命令更新:
|
||||||
|
|
||||||
|
docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/krau/saveany-bot:latest
|
docker pull ghcr.io/krau/saveany-bot:latest
|
||||||
docker restart saveany-bot
|
docker restart saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
docker compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose restart
|
||||||
```
|
```
|
||||||
@@ -11,7 +11,7 @@ if [ -n "$CONFIG_URL" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f /app/config.toml ]; then
|
if [ ! -f /app/config.toml ]; then
|
||||||
echo "[ERROR] Missing config.toml: 请通过挂载或 CONFIG_URL 提供配置文件"
|
echo "[ERROR] Missing config.toml: Please provide the configuration file via mounting or CONFIG_URL"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
66
go.mod
66
go.mod
@@ -4,52 +4,54 @@ go 1.24.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/celestix/gotgproto v1.0.0-beta21.0.20251018053725-7e5db4fdbe66
|
github.com/celestix/gotgproto v1.0.0-beta22
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/charmbracelet/huh v0.8.0
|
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/fatih/color v1.18.0
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10
|
github.com/gabriel-vasile/mimetype v1.4.10
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/gotd/contrib v0.21.1
|
github.com/gotd/contrib v0.21.1
|
||||||
github.com/gotd/td v0.132.0
|
github.com/gotd/td v0.136.0
|
||||||
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0
|
||||||
github.com/minio/minio-go/v7 v7.0.95
|
github.com/minio/minio-go/v7 v7.0.95
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/playwright-community/playwright-go v0.5200.1
|
||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
golang.org/x/net v0.46.0
|
github.com/unvgo/ghselfupdate v1.0.0
|
||||||
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||||
|
golang.org/x/net v0.47.0
|
||||||
|
golang.org/x/term v0.37.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AnimeKaizoku/cacher v1.0.3 // indirect
|
github.com/AnimeKaizoku/cacher v1.0.3 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/catppuccin/go v0.3.0 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20251023181713-f594ac034d6b // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
|
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
github.com/go-faster/jx v1.1.0 // indirect
|
github.com/go-faster/jx v1.2.0 // indirect
|
||||||
github.com/go-faster/xor v1.0.0 // indirect
|
github.com/go-faster/xor v1.0.0 // indirect
|
||||||
github.com/go-faster/yaml v0.4.6 // indirect
|
github.com/go-faster/yaml v0.4.6 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
@@ -65,39 +67,33 @@ require (
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/ncruces/julianday v1.0.0 // indirect
|
github.com/ncruces/julianday v1.0.0 // indirect
|
||||||
github.com/ogen-go/ogen v1.16.0 // indirect
|
github.com/ogen-go/ogen v1.16.0 // indirect
|
||||||
github.com/onsi/gomega v1.36.2 // indirect
|
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
github.com/tetratelabs/wazero v1.10.1 // indirect
|
||||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
|
||||||
github.com/tinylib/msgp v1.4.0 // indirect
|
github.com/tinylib/msgp v1.4.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/oauth2 v0.32.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.66.10 // indirect
|
modernc.org/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
@@ -111,12 +107,12 @@ require (
|
|||||||
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6
|
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6
|
||||||
github.com/duke-git/lancet/v2 v2.3.7
|
github.com/duke-git/lancet/v2 v2.3.7
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1
|
github.com/ncruces/go-sqlite3 v0.30.1
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
@@ -125,10 +121,10 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.18.0
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.30.0
|
golang.org/x/text v0.31.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|||||||
233
go.sum
233
go.sum
@@ -2,34 +2,48 @@ github.com/AnimeKaizoku/cacher v1.0.3 h1:foNAmLfY/DXfA4yEy4uP6WK2Ni7JC+s3QhZv72D
|
|||||||
github.com/AnimeKaizoku/cacher v1.0.3/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
github.com/AnimeKaizoku/cacher v1.0.3/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75 h1:S61/E3N01oral6B3y9hZ2E1iFDqCZPPOBoBQretCnBI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75/go.mod h1:bDMQbkI1vJbNjnvJYpPTSNYBkI/VIv18ngWb/K84tkk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 h1:4nm2G6A4pV9rdlWzGMPv4BNtQp22v1hg3yrtkYpeLl8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k=
|
||||||
|
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||||
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
github.com/celestix/gotgproto v1.0.0-beta22 h1:Iu78cFA08nV8+flmxKs9CJ3W73+HG30fx0nLOs5A6fI=
|
||||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/celestix/gotgproto v1.0.0-beta22/go.mod h1:JYC9Js/5KLUhFR5M2RslQi2DFAcF7EdrgJMXo0YrzGQ=
|
||||||
github.com/celestix/gotgproto v1.0.0-beta21.0.20251018053725-7e5db4fdbe66 h1:yo+mL8D/9viQdrXoq3W7uaR5wRPaUiGDAw50vmOJtbs=
|
|
||||||
github.com/celestix/gotgproto v1.0.0-beta21.0.20251018053725-7e5db4fdbe66/go.mod h1:JYC9Js/5KLUhFR5M2RslQi2DFAcF7EdrgJMXo0YrzGQ=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
||||||
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
|
||||||
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
@@ -38,29 +52,18 @@ github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvA
|
|||||||
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
|
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
|
||||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20251023181713-f594ac034d6b h1:LUXEpSryQXJyP2lwAi/vBto9n+cJzlXSIefnCol3FVw=
|
|
||||||
github.com/charmbracelet/x/exp/strings v0.0.0-20251023181713-f594ac034d6b/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
||||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
||||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
|
||||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||||
@@ -73,13 +76,10 @@ github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HH
|
|||||||
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
@@ -92,8 +92,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
|
|||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||||
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
|
||||||
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
|
||||||
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||||
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
||||||
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||||
@@ -101,6 +101,8 @@ github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
|||||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
@@ -109,15 +111,17 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
|
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||||
|
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||||
@@ -135,9 +139,8 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
|||||||
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
||||||
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||||
github.com/gotd/td v0.132.0 h1:Iqm3S2b+8kDgA9237IDXRxj7sryUpvy+4Cr50/0tpx4=
|
github.com/gotd/td v0.136.0 h1:f7vx/1rlvP59L5EKR820XpMRO2k267wW8/F0rAWbepc=
|
||||||
github.com/gotd/td v0.132.0/go.mod h1:4CDGYS+rDtOqotRheGaF9MS5g6jaUewvSXqBNJnx8SQ=
|
github.com/gotd/td v0.136.0/go.mod h1:mStcqs/9FXhNhWnPTguptSwqkQbRIwXLw3SCSpzPJxM=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -146,27 +149,25 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3 h1:2713fQZ560HxoNVgfJH41GKzjMjIG+DW4hH6nYXfXW8=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3/go.mod h1:S4S9jGBVlLri0OeqrSSbCGG5vsI6he06UJyuz1WT1EE=
|
||||||
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0 h1:F4HWvOrKXQsfLsFTOnUfP0HY6WISJqOrsAFGSIzkKto=
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0/go.mod h1:sa7/bWHB6fO9j4lhmxnWQ1U07o+dE1leFjhctotxU7A=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
@@ -175,20 +176,16 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
|||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1 h1:NIi8AISWBToRHyoz01FXiTNvU147Tqdibgj2tFzJCqM=
|
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1/go.mod h1:PpccBNNhvjwUOwDQEn2gXQPFPTWdlromj0+fSkd5KSg=
|
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0 h1:81sHeq3CCdhjoqAB650n5wEdRlLO9VBvosArskcN3+c=
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1 h1:kApjSKrepgmhtx63KMeD8aUoz1l4aJT4fkoBmHSsRns=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0/go.mod h1:vXfVWdBfg7qOgqQqHpzUWl9LLswD0h+8mK4oouaV2oc=
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1/go.mod h1:zgFibXnnKek3qMHd/2A1OtfDqbN7ae+H80aMX+487As=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||||
@@ -197,22 +194,18 @@ github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpM
|
|||||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||||
github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw=
|
github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw=
|
||||||
github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI=
|
github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
|
||||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/playwright-community/playwright-go v0.5200.1 h1:Sm2oOuhqt0M5Y4kUi/Qh9w4cyyi3ZIWTBeGKImc2UVo=
|
||||||
|
github.com/playwright-community/playwright-go v0.5200.1/go.mod h1:UnnyQZaqUOO5ywAZu60+N4EiWReUqX1MQBBA3Oofvf8=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -220,6 +213,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
|||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
@@ -237,21 +232,28 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
|
||||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
|
||||||
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
||||||
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/unvgo/ghselfupdate v1.0.0 h1:XNdk2sr9ESgmVWlUj6y3XQnutczv2SLToCBENtUkIYE=
|
||||||
|
github.com/unvgo/ghselfupdate v1.0.0/go.mod h1:3snWV5vEHGXQqqhY7FwKjPOtH6e7cFdHYN7UMAihhxs=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c h1:xA2TJS9Hu/ivzaZIrDcwvpJ3Fnpsk5fDOJ4iSnL6J0w=
|
||||||
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
@@ -260,71 +262,94 @@ go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgf
|
|||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
|
||||||
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package parsers
|
//go:build !no_jsparser
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,48 +13,51 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/parsers/parsers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
||||||
return func(call goja.FunctionCall) goja.Value {
|
return func(call goja.FunctionCall) goja.Value {
|
||||||
jsObj := call.Argument(0)
|
jsObj := call.Argument(0)
|
||||||
if jsObj == nil || goja.IsUndefined(jsObj) || goja.IsNull(jsObj) {
|
if jsObj == nil || goja.IsUndefined(jsObj) || goja.IsNull(jsObj) {
|
||||||
panic("registerParser expects an object { canHandle, parse }")
|
return vm.NewGoError(errors.New("registerParser expects an object { canHandle, parse }"))
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := jsObj.ToObject(vm)
|
obj := jsObj.ToObject(vm)
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
panic("registerParser: cannot convert argument to object")
|
return vm.NewGoError(errors.New("registerParser expects an object { canHandle, parse }"))
|
||||||
}
|
}
|
||||||
metaValue := obj.Get("metadata")
|
metaValue := obj.Get("metadata")
|
||||||
if metaValue == nil || goja.IsUndefined(metaValue) {
|
if metaValue == nil || goja.IsUndefined(metaValue) {
|
||||||
panic("parser must provide metadata")
|
return vm.NewGoError(errors.New("parser must provide metadata"))
|
||||||
}
|
}
|
||||||
var metadata PluginMeta
|
var metadata PluginMeta
|
||||||
if exported := metaValue.Export(); exported != nil {
|
if exported := metaValue.Export(); exported != nil {
|
||||||
data, err := json.Marshal(exported)
|
data, err := json.Marshal(exported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("failed to marshal metadata to JSON: %v", err))
|
return vm.NewGoError(fmt.Errorf("failed to marshal metadata to JSON: %w", err))
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||||
panic(fmt.Sprintf("failed to unmarshal JSON to PluginMeta: %v", err))
|
return vm.NewGoError(fmt.Errorf("failed to unmarshal JSON to PluginMeta: %w", err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic("metadata cannot be null or undefined")
|
return vm.NewGoError(errors.New("metadata cannot be null or undefined"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginV := semver.MustParse(metadata.Version)
|
pluginV := semver.MustParse(metadata.Version)
|
||||||
if pluginV.LT(MinimumParserVersion) || pluginV.GT(LatestParserVersion) {
|
if pluginV.LT(MinimumParserVersion) {
|
||||||
panic(fmt.Sprintf("parser version %s is not supported, must be between %s and %s", metadata.Version, MinimumParserVersion, LatestParserVersion))
|
return vm.NewGoError(fmt.Errorf("parser version %s is not supported, must be at least %s", metadata.Version, MinimumParserVersion))
|
||||||
|
}
|
||||||
|
if pluginV.Major > LatestParserVersion.Major {
|
||||||
|
log.Printf("warning: parser major version %d is newer than latest supported major version %d", pluginV.Major, LatestParserVersion.Major)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFn := obj.Get("canHandle")
|
handleFn := obj.Get("canHandle")
|
||||||
parseFn := obj.Get("parse")
|
parseFn := obj.Get("parse")
|
||||||
if parseFn == nil || goja.IsUndefined(parseFn) {
|
if parseFn == nil || goja.IsUndefined(parseFn) {
|
||||||
panic("parser must provide a parse function")
|
return vm.NewGoError(errors.New("parser must provide a parse function"))
|
||||||
}
|
}
|
||||||
|
parsers.Add(newJSParser(vm, handleFn, parseFn, metadata))
|
||||||
parsers = append(parsers, newJSParser(vm, handleFn, parseFn, metadata))
|
|
||||||
return goja.Undefined()
|
return goja.Undefined()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,9 +74,27 @@ var jsConsole = func(logger *log.Logger) map[string]any {
|
|||||||
logger.Info(args[0])
|
logger.Info(args[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"error": func(args ...any) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
logger.Error(fmt.Sprint(args[0]), args[1:]...)
|
||||||
|
} else {
|
||||||
|
logger.Error(fmt.Sprint(args[0]))
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
jsGhttp provides a http helper for js plugins
|
||||||
|
|
||||||
|
It provides the following functions:
|
||||||
|
- get(url): performs a GET request and returns the response body as string
|
||||||
|
- getJSON(url): performs a GET request and returns the response body parsed as JSON
|
||||||
|
- head(url): performs a HEAD request and returns the response headers and status code
|
||||||
|
*/
|
||||||
var jsGhttp = func(vm *goja.Runtime) *goja.Object {
|
var jsGhttp = func(vm *goja.Runtime) *goja.Object {
|
||||||
ghttp := vm.NewObject()
|
ghttp := vm.NewObject()
|
||||||
client := netutil.DefaultParserHTTPClient()
|
client := netutil.DefaultParserHTTPClient()
|
||||||
84
parsers/js/api_playwright.go
Normal file
84
parsers/js/api_playwright.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//go:build !no_jsparser && !no_playwright
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/playwright-community/playwright-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsPlaywright = func(vm *goja.Runtime, logger *log.Logger) *goja.Object {
|
||||||
|
pwObj := vm.NewObject()
|
||||||
|
var installOnce sync.Once
|
||||||
|
slogger := slog.New(logger)
|
||||||
|
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
||||||
|
url := call.Argument(0).String()
|
||||||
|
var installErr error
|
||||||
|
installOnce.Do(func() {
|
||||||
|
installErr = playwright.Install(&playwright.RunOptions{
|
||||||
|
Browsers: []string{"chromium"},
|
||||||
|
DriverDirectory: "./playwright",
|
||||||
|
Logger: slogger,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if installErr != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to install playwright: %v", installErr),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pw, err := playwright.Run(&playwright.RunOptions{
|
||||||
|
DriverDirectory: "./playwright",
|
||||||
|
Logger: slogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to start playwright: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer pw.Stop()
|
||||||
|
|
||||||
|
browser, err := pw.Chromium.Launch()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to launch browser: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer browser.Close()
|
||||||
|
|
||||||
|
page, err := browser.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to create page: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := page.Goto(url, playwright.PageGotoOptions{
|
||||||
|
WaitUntil: playwright.WaitUntilStateNetworkidle,
|
||||||
|
Timeout: playwright.Float(60000),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to navigate: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if resp != nil && resp.Status() >= 400 {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("bad status code: %d", resp.Status()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
content, err := page.Content()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to get page content: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vm.ToValue(content)
|
||||||
|
})
|
||||||
|
return pwObj
|
||||||
|
}
|
||||||
19
parsers/js/api_playwright_stub.go
Normal file
19
parsers/js/api_playwright_stub.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build no_playwright && !no_jsparser
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsPlaywright = func(vm *goja.Runtime, _ *log.Logger) *goja.Object {
|
||||||
|
pwObj := vm.NewObject()
|
||||||
|
unsupported := vm.ToValue(map[string]any{
|
||||||
|
"error": "playwright is not supported in this build",
|
||||||
|
})
|
||||||
|
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
||||||
|
return unsupported
|
||||||
|
})
|
||||||
|
return pwObj
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package parsers
|
//go:build !no_jsparser
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,9 +8,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +102,7 @@ func newJSParser(vm *goja.Runtime, canHandleFunc, parseFunc goja.Value, metadata
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载指定文件夹下的所有 JS 解析器插件
|
||||||
func LoadPlugins(ctx context.Context, dir string) error {
|
func LoadPlugins(ctx context.Context, dir string) error {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,10 +126,49 @@ func LoadPlugins(ctx context.Context, dir string) error {
|
|||||||
vm.Set("console", jsConsole(logger))
|
vm.Set("console", jsConsole(logger))
|
||||||
// http fetch funcs
|
// http fetch funcs
|
||||||
vm.Set("ghttp", jsGhttp(vm))
|
vm.Set("ghttp", jsGhttp(vm))
|
||||||
|
// playwright fetch func
|
||||||
|
vm.Set("playwright", jsPlaywright(vm, logger))
|
||||||
|
|
||||||
if _, err := vm.RunString(string(code)); err != nil {
|
if _, err := vm.RunString(string(code)); err != nil {
|
||||||
return fmt.Errorf("error loading plugin %s: %w", e.Name(), err)
|
return fmt.Errorf("error loading plugin %s: %w", e.Name(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pluginNameMu sync.Map
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddPlugin(ctx context.Context, code string, name string) error {
|
||||||
|
value, _ := pluginNameMu.LoadOrStore(name, &sync.Mutex{})
|
||||||
|
mu := value.(*sync.Mutex)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
return addPlugin(ctx, code, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPlugin(ctx context.Context, code string, name string) error {
|
||||||
|
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("[plugin|parser]/%s", name))
|
||||||
|
vm := goja.New()
|
||||||
|
vm.Set("registerParser", jsRegisterParser(vm))
|
||||||
|
vm.Set("console", jsConsole(logger))
|
||||||
|
vm.Set("ghttp", jsGhttp(vm))
|
||||||
|
vm.Set("playwright", jsPlaywright(vm, logger))
|
||||||
|
if _, err := vm.RunString(code); err != nil {
|
||||||
|
return fmt.Errorf("error loading plugin %s: %w", name, err)
|
||||||
|
}
|
||||||
|
dir := "plugins"
|
||||||
|
configuredDirs := config.C().Parser.PluginDirs
|
||||||
|
if len(configuredDirs) > 0 {
|
||||||
|
dir = configuredDirs[0]
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||||
|
pluginPath := filepath.Join(dir, name)
|
||||||
|
if err := os.WriteFile(pluginPath, []byte(code), 0644); err != nil {
|
||||||
|
logger.Warn("Failed to save plugin file: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
16
parsers/js/js_stub.go
Normal file
16
parsers/js/js_stub.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build no_jsparser
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadPlugins(ctx context.Context, dir string) error {
|
||||||
|
return errors.New("JS parser plugins are not supported in this build")
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPlugin(ctx context.Context, code string, name string) error {
|
||||||
|
return errors.New("JS parser plugins are not supported in this build")
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package parsers
|
//go:build !no_jsparser
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
import "github.com/blang/semver"
|
import "github.com/blang/semver"
|
||||||
|
|
||||||
@@ -3,41 +3,16 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/parsers/js"
|
||||||
"github.com/krau/SaveAny-Bot/parsers/kemono"
|
"github.com/krau/SaveAny-Bot/parsers/native/kemono"
|
||||||
"github.com/krau/SaveAny-Bot/parsers/twitter"
|
"github.com/krau/SaveAny-Bot/parsers/native/twitter"
|
||||||
|
"github.com/krau/SaveAny-Bot/parsers/parsers"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
parsers []parser.Parser
|
|
||||||
parsersMu sync.Mutex
|
|
||||||
doConfig sync.Once
|
|
||||||
configParsers = func() {
|
|
||||||
if len(parsers) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, pser := range parsers {
|
|
||||||
if configurable, ok := pser.(parser.ConfigurableParser); ok {
|
|
||||||
cfg := config.C().GetParserConfigByName(configurable.Name())
|
|
||||||
if err := configurable.Configure(cfg); err != nil {
|
|
||||||
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddParser(p ...parser.Parser) {
|
|
||||||
parsersMu.Lock()
|
|
||||||
defer parsersMu.Unlock()
|
|
||||||
parsers = append(parsers, p...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
AddParser(new(twitter.TwitterParser), new(kemono.KemonoParser))
|
parsers.Add(new(twitter.TwitterParser), new(kemono.KemonoParser))
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -45,12 +20,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
||||||
doConfig.Do(configParsers)
|
|
||||||
ch := make(chan *parser.Item, 1)
|
ch := make(chan *parser.Item, 1)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, pser := range parsers {
|
for _, pser := range parsers.Get() {
|
||||||
if !pser.CanHandle(url) {
|
if !pser.CanHandle(url) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -75,12 +49,20 @@ func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanHandle checks if any registered parser can handle the given URL and returns the parser if found.
|
||||||
func CanHandle(url string) (bool, parser.Parser) {
|
func CanHandle(url string) (bool, parser.Parser) {
|
||||||
doConfig.Do(configParsers)
|
for _, pser := range parsers.Get() {
|
||||||
for _, pser := range parsers {
|
|
||||||
if pser.CanHandle(url) {
|
if pser.CanHandle(url) {
|
||||||
return true, pser
|
return true, pser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadPlugins(ctx context.Context, dir string) error {
|
||||||
|
return js.LoadPlugins(ctx, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPlugin(ctx context.Context, code string, name string) error {
|
||||||
|
return js.AddPlugin(ctx, code, name)
|
||||||
|
}
|
||||||
|
|||||||
43
parsers/parsers/parsers.go
Normal file
43
parsers/parsers/parsers.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
parsers []parser.Parser
|
||||||
|
mu sync.Mutex
|
||||||
|
configOnce sync.Once
|
||||||
|
configParsers = func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(parsers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, pser := range parsers {
|
||||||
|
if configurable, ok := pser.(parser.ConfigurableParser); ok {
|
||||||
|
cfg := config.C().GetParserConfigByName(configurable.Name())
|
||||||
|
if err := configurable.Configure(cfg); err != nil {
|
||||||
|
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Add(p ...parser.Parser) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
parsers = append(parsers, p...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() []parser.Parser {
|
||||||
|
configOnce.Do(configParsers)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return parsers
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by go-enum DO NOT EDIT.
|
// Code generated by go-enum DO NOT EDIT.
|
||||||
// Version: 0.6.1
|
// Version: 0.9.1
|
||||||
// Revision: a6f63bddde05aca4221df9c8e9e6d7d9674b1cb4
|
// Revision: 42b1ed55945781de07471bb2db52b3f9edee19b0
|
||||||
// Build Date: 2025-03-18T23:42:14Z
|
// Build Date: 2025-08-02T17:25:40Z
|
||||||
// Built By: goreleaser
|
// Built By: goreleaser
|
||||||
|
|
||||||
package ctxkey
|
package ctxkey
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by go-enum DO NOT EDIT.
|
// Code generated by go-enum DO NOT EDIT.
|
||||||
// Version: 0.6.1
|
// Version: 0.9.1
|
||||||
// Revision: a6f63bddde05aca4221df9c8e9e6d7d9674b1cb4
|
// Revision: 42b1ed55945781de07471bb2db52b3f9edee19b0
|
||||||
// Build Date: 2025-03-18T23:42:14Z
|
// Build Date: 2025-08-02T17:25:40Z
|
||||||
// Built By: goreleaser
|
// Built By: goreleaser
|
||||||
|
|
||||||
package fnamest
|
package fnamest
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ package storage
|
|||||||
|
|
||||||
// StorageType
|
// StorageType
|
||||||
/* ENUM(
|
/* ENUM(
|
||||||
local, webdav, alist, minio, telegram
|
local, webdav, alist, minio, telegram, s3
|
||||||
) */
|
) */
|
||||||
type StorageType string
|
type StorageType string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by go-enum DO NOT EDIT.
|
// Code generated by go-enum DO NOT EDIT.
|
||||||
// Version: 0.6.1
|
// Version: 0.9.1
|
||||||
// Revision: a6f63bddde05aca4221df9c8e9e6d7d9674b1cb4
|
// Revision: 42b1ed55945781de07471bb2db52b3f9edee19b0
|
||||||
// Build Date: 2025-03-18T23:42:14Z
|
// Build Date: 2025-08-02T17:25:40Z
|
||||||
// Built By: goreleaser
|
// Built By: goreleaser
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
@@ -22,6 +22,8 @@ const (
|
|||||||
Minio StorageType = "minio"
|
Minio StorageType = "minio"
|
||||||
// Telegram is a StorageType of type telegram.
|
// Telegram is a StorageType of type telegram.
|
||||||
Telegram StorageType = "telegram"
|
Telegram StorageType = "telegram"
|
||||||
|
// S3 is a StorageType of type s3.
|
||||||
|
S3 StorageType = "s3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidStorageType = fmt.Errorf("not a valid StorageType, try [%s]", strings.Join(_StorageTypeNames, ", "))
|
var ErrInvalidStorageType = fmt.Errorf("not a valid StorageType, try [%s]", strings.Join(_StorageTypeNames, ", "))
|
||||||
@@ -32,6 +34,7 @@ var _StorageTypeNames = []string{
|
|||||||
string(Alist),
|
string(Alist),
|
||||||
string(Minio),
|
string(Minio),
|
||||||
string(Telegram),
|
string(Telegram),
|
||||||
|
string(S3),
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageTypeNames returns a list of possible string values of StorageType.
|
// StorageTypeNames returns a list of possible string values of StorageType.
|
||||||
@@ -49,6 +52,7 @@ func StorageTypeValues() []StorageType {
|
|||||||
Alist,
|
Alist,
|
||||||
Minio,
|
Minio,
|
||||||
Telegram,
|
Telegram,
|
||||||
|
S3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +74,7 @@ var _StorageTypeValue = map[string]StorageType{
|
|||||||
"alist": Alist,
|
"alist": Alist,
|
||||||
"minio": Minio,
|
"minio": Minio,
|
||||||
"telegram": Telegram,
|
"telegram": Telegram,
|
||||||
|
"s3": S3,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseStorageType attempts to convert a string to a StorageType.
|
// ParseStorageType attempts to convert a string to a StorageType.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package tasktype
|
package tasktype
|
||||||
|
|
||||||
//go:generate go-enum --values --names --flag --nocase
|
//go:generate go-enum --values --names --flag --nocase
|
||||||
// ENUM(tgfiles,tphpics,parseditem)
|
// ENUM(tgfiles,tphpics,parseditem,directlinks)
|
||||||
type TaskType string
|
type TaskType string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by go-enum DO NOT EDIT.
|
// Code generated by go-enum DO NOT EDIT.
|
||||||
// Version: 0.6.1
|
// Version: 0.9.1
|
||||||
// Revision: a6f63bddde05aca4221df9c8e9e6d7d9674b1cb4
|
// Revision: 42b1ed55945781de07471bb2db52b3f9edee19b0
|
||||||
// Build Date: 2025-03-18T23:42:14Z
|
// Build Date: 2025-08-02T17:25:40Z
|
||||||
// Built By: goreleaser
|
// Built By: goreleaser
|
||||||
|
|
||||||
package tasktype
|
package tasktype
|
||||||
@@ -18,6 +18,8 @@ const (
|
|||||||
TaskTypeTphpics TaskType = "tphpics"
|
TaskTypeTphpics TaskType = "tphpics"
|
||||||
// TaskTypeParseditem is a TaskType of type parseditem.
|
// TaskTypeParseditem is a TaskType of type parseditem.
|
||||||
TaskTypeParseditem TaskType = "parseditem"
|
TaskTypeParseditem TaskType = "parseditem"
|
||||||
|
// TaskTypeDirectlinks is a TaskType of type directlinks.
|
||||||
|
TaskTypeDirectlinks TaskType = "directlinks"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidTaskType = fmt.Errorf("not a valid TaskType, try [%s]", strings.Join(_TaskTypeNames, ", "))
|
var ErrInvalidTaskType = fmt.Errorf("not a valid TaskType, try [%s]", strings.Join(_TaskTypeNames, ", "))
|
||||||
@@ -26,6 +28,7 @@ var _TaskTypeNames = []string{
|
|||||||
string(TaskTypeTgfiles),
|
string(TaskTypeTgfiles),
|
||||||
string(TaskTypeTphpics),
|
string(TaskTypeTphpics),
|
||||||
string(TaskTypeParseditem),
|
string(TaskTypeParseditem),
|
||||||
|
string(TaskTypeDirectlinks),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskTypeNames returns a list of possible string values of TaskType.
|
// TaskTypeNames returns a list of possible string values of TaskType.
|
||||||
@@ -41,6 +44,7 @@ func TaskTypeValues() []TaskType {
|
|||||||
TaskTypeTgfiles,
|
TaskTypeTgfiles,
|
||||||
TaskTypeTphpics,
|
TaskTypeTphpics,
|
||||||
TaskTypeParseditem,
|
TaskTypeParseditem,
|
||||||
|
TaskTypeDirectlinks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +61,10 @@ func (x TaskType) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _TaskTypeValue = map[string]TaskType{
|
var _TaskTypeValue = map[string]TaskType{
|
||||||
"tgfiles": TaskTypeTgfiles,
|
"tgfiles": TaskTypeTgfiles,
|
||||||
"tphpics": TaskTypeTphpics,
|
"tphpics": TaskTypeTphpics,
|
||||||
"parseditem": TaskTypeParseditem,
|
"parseditem": TaskTypeParseditem,
|
||||||
|
"directlinks": TaskTypeDirectlinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTaskType attempts to convert a string to a TaskType.
|
// ParseTaskType attempts to convert a string to a TaskType.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user