Skip to content

Commit 83900e7

Browse files
authored
Merge pull request #675 from pavelpikta/feature/tg-bot
feat: add new telegram bot commands
2 parents d266990 + 4bda08b commit 83900e7

54 files changed

Lines changed: 5543 additions & 216 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ On FreeBSD (TrueNAS/FreeNAS) you can use this plugin: <https://github.com/filka9
176176
- `--pubipv6 PUBIPV6`, `-6 PUBIPV6` - set public IPv6 addr
177177
- `--searchwa`, `-s` - allow search without authentication
178178
- `--maxsize MAXSIZE`, `-m MAXSIZE` - max allowed stream size (in Bytes)
179-
- `--tg TGTOKEN`, `-T TGTOKEN` - telegram bot token
179+
- `--tg TGTOKEN`, `-T TGTOKEN` - [Telegram bot](server/tgbot/README.md) token
180180
- `--fuse FUSEPATH`, `-f FUSEPATH` - fuse mount path
181181
- `--webdav` - enable web dav
182182
- `--proxyurl PROXYURL` - set proxy URL for BitTorrent traffic (http, socks4, socks5, socks5h), example: socks5h://user:password@example.com:2080

server/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ func Start() {
7070
settings.IP = settings.Args.IP
7171

7272
if settings.Args.TGToken != "" {
73-
tgbot.Start(settings.Args.TGToken)
73+
if err := tgbot.Start(settings.Args.TGToken); err != nil {
74+
log.TLogln("tg bot start failed", err)
75+
}
7476
}
7577
web.Start()
7678
}

server/tgbot/README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# TorrServer Telegram Bot
2+
3+
[![GitHub License](https://img.shields.io/github/license/YouROK/TorrServer)](https://github.com/YouROK/TorrServer/blob/master/LICENSE)
4+
[![TorrServer Integrated](https://img.shields.io/badge/TorrServer-integrated-blue)](https://github.com/YouROK/TorrServer)
5+
6+
## Introduction
7+
8+
Telegram bot for managing [TorrServer](https://github.com/YouROK/TorrServer) — add torrents, stream, search, and control the server directly from Telegram.
9+
10+
## Features
11+
12+
- Torrent management — add, remove, drop, list via magnet, hash, or `torrs://`
13+
- Export & import — magnets list; import multiple from text
14+
- Streaming — playback links, M3U playlists, preload
15+
- Search — RuTor and Torznab with one-click add
16+
- Inline mode — `@botname` in any chat: list torrents or search
17+
- Status & snake — real-time status, cache visualization
18+
- File operations — browse files, download to Telegram
19+
- FFprobe — media metadata via `/ffp`
20+
- Localization — Russian and English
21+
- Admin — shutdown, settings, presets (whitelist users only)
22+
23+
## Getting Started
24+
25+
### Enable the Bot
26+
27+
Start TorrServer with a Telegram bot token:
28+
29+
```bash
30+
TorrServer --tg YOUR_BOT_TOKEN
31+
```
32+
33+
Or use `-T`:
34+
35+
```bash
36+
TorrServer -T YOUR_BOT_TOKEN
37+
```
38+
39+
Create a bot via [@BotFather](https://t.me/BotFather) to get the token.
40+
41+
### Configuration
42+
43+
Config file `tg.cfg` (JSON) in the TorrServer data directory:
44+
45+
| Field | Description |
46+
|------------|-------------|
47+
| `HostTG` | Telegram API URL (default: `https://api.telegram.org`) |
48+
| `HostWeb` | Base URL for stream links (auto-detected if empty) |
49+
| `WhiteIds` | Allowed user IDs (empty = allow all) |
50+
| `BlackIds` | Blocked user IDs |
51+
52+
Example:
53+
54+
```json
55+
{
56+
"HostTG": "https://api.telegram.org",
57+
"HostWeb": "http://192.168.1.100:8090",
58+
"WhiteIds": [123456789],
59+
"BlackIds": []
60+
}
61+
```
62+
63+
## Commands
64+
65+
### Core
66+
67+
| Command | Description |
68+
|---------|-------------|
69+
| `/help`, `/start`, `/id` | Help and user ID |
70+
| `/list [compact]` | List torrents with buttons |
71+
| `/add <link>` | Add torrent (magnet, hash, torrs://) |
72+
| `/clear` | Remove all (with confirmation) |
73+
| `/hash [N]` | Show info hashes |
74+
75+
### Management
76+
77+
| Command | Description |
78+
|---------|-------------|
79+
| `/remove <hash\|N>` | Remove torrent |
80+
| `/drop <hash\|N>` | Disconnect (keep in DB) |
81+
| `/set <hash\|N> <title>` | Set title |
82+
| `/status [hash\|N]` | Status with refresh/stop |
83+
| `/cache <hash\|N>` | Cache stats |
84+
| `/preload <hash\|N> <index>` | Preload file |
85+
86+
### Links & Playback
87+
88+
| Command | Description |
89+
|---------|-------------|
90+
| `/link`, `/play` | Stream URL |
91+
| `/m3u`, `/m3uall` | M3U playlist |
92+
93+
### Search
94+
95+
| Command | Description |
96+
|---------|-------------|
97+
| `/search <query>` | RuTor + Torznab (all sources) |
98+
| `/rutor <query>` | RuTor only |
99+
| `/torznab <query> [index]` | Torznab indexers |
100+
101+
### Other
102+
103+
| Command | Description |
104+
|---------|-------------|
105+
| `/export`, `/import` | Export/import magnets |
106+
| `/categories` | List categories |
107+
| `/server`, `/stats`, `/stat` | Server info |
108+
| `/viewed` | Viewed files |
109+
| `/ffp <hash\|N> <id> [json]` | FFprobe metadata |
110+
| `/speedtest [size]` | Download test (1–100 MB) |
111+
| `/snake [hash\|N] [cols] [rows]` | Cache visualization |
112+
| `/lang [RU\|EN]` | Language |
113+
114+
### Admin Only
115+
116+
| Command | Description |
117+
|---------|-------------|
118+
| `/shutdown` | Shut down server |
119+
| `/settings` | Interactive settings menu (sub-pages: Search, Network, Other, Cache, Paths, Storage) |
120+
| `/preset <name>` | Apply named preset: `performance`, `storage`, `streaming`, `low`, `default` |
121+
| `/preset <key> <value> ...` | Apply key-value pairs: `cache 256`, `preload 50`, `conn 100`, etc. |
122+
123+
**Preset examples:**
124+
- `/preset performance` — max cache, high preload, no limits
125+
- `/preset cache 256 preload 50` — set cache 256 MB and preload 50%
126+
- `/preset cache 512 conn 100 down 0 up 0` — multiple values
127+
128+
**Preset keys:** `cache`, `preload`, `readahead`, `conn`, `timeout`, `port`, `down`, `up`, `retr`, `responsive`, `cachedrop`
129+
130+
## Inline Mode
131+
132+
Type `@YourBotName` in any chat:
133+
134+
- **Empty, "list", or "play"** — torrents with play links
135+
- **2+ characters** — search RuTor + Torznab
136+
137+
## Text Input
138+
139+
Paste as plain message to add torrent:
140+
141+
- `magnet:?xt=urn:btih:...`
142+
- `torrs://...`
143+
- 40-char info hash
144+
145+
Reply to file list with `2-12` to download files 2–12 to Telegram.
146+
147+
## Security
148+
149+
- **Whitelist** — restrict to specific user IDs
150+
- **Blacklist** — block user IDs
151+
- **Admin** — when whitelist is used, admin = whitelisted users
152+
- **Settings** — sensitive values masked in `/settings`
153+
154+
## Dependencies
155+
156+
- [telebot v4](https://gopkg.in/telebot.v4) — Telegram Bot API
157+
- [go-humanize](https://github.com/dustin/go-humanize)
158+
- [go-ffprobe](https://gopkg.in/vansante/go-ffprobe.v2)

server/tgbot/add.go

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,53 @@ package tgbot
22

33
import (
44
"errors"
5+
"fmt"
6+
"io"
57
"strings"
68

9+
"github.com/anacrolix/torrent"
710
tele "gopkg.in/telebot.v4"
811
"server/log"
912
set "server/settings"
1013
"server/torr"
1114
"server/web/api/utils"
1215
)
1316

14-
func addTorrent(c tele.Context, link string) error {
15-
msg, err := c.Bot().Send(c.Sender(), "Подключение к торренту...")
16-
if err != nil {
17-
return err
18-
}
19-
log.TLogln("tg add torrent", link)
20-
link = strings.ReplaceAll(link, "&amp;", "&")
21-
torrSpec, err := utils.ParseLink(link)
17+
func addTorrentFromSpec(c tele.Context, torrSpec *torrent.TorrentSpec, displayLabel string) error {
18+
msg, err := c.Bot().Send(c.Sender(), tr(c.Sender().ID, "connecting"))
2219
if err != nil {
23-
log.TLogln("tg error parse link:", err)
2420
return err
2521
}
2622

2723
tor, err := torr.AddTorrent(torrSpec, "", "", "", "")
28-
29-
if tor.Data != "" && set.BTsets.EnableDebug {
30-
log.TLogln("torrent data:", tor.Data)
24+
if err != nil {
25+
log.TLogln("tg add err", err)
26+
_, _ = c.Bot().Edit(msg, fmt.Sprintf(tr(c.Sender().ID, "add_error"), err.Error()))
27+
return err
3128
}
32-
if tor.Category != "" && set.BTsets.EnableDebug {
33-
log.TLogln("torrent category:", tor.Category)
29+
if tor == nil {
30+
_, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_not_created"))
31+
return errors.New("torrent not created")
3432
}
3533

36-
if err != nil {
37-
log.TLogln("tg error add torrent:", err)
38-
c.Bot().Edit(msg, "Ошибка при подключении: "+err.Error())
39-
return err
34+
if set.BTsets != nil && set.BTsets.EnableDebug {
35+
if tor.Data != "" {
36+
log.TLogln("tg add data", logSafeStr(tor.Data, 60))
37+
}
38+
if tor.Category != "" {
39+
log.TLogln("tg add category", logSafeStr(tor.Category, 40))
40+
}
4041
}
4142

43+
_, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_getting_meta"))
4244
if !tor.GotInfo() {
43-
log.TLogln("tg error add torrent: timeout connection get torrent info")
44-
c.Bot().Edit(msg, "Ошибка при добаваления торрента: timeout connection get torrent info")
45+
log.TLogln("tg add err", "timeout get torrent info")
46+
_, _ = c.Bot().Edit(msg, tr(c.Sender().ID, "add_timeout"))
4547
return errors.New("timeout connection get torrent info")
4648
}
4749

4850
if tor.Title == "" {
49-
tor.Title = torrSpec.DisplayName // prefer dn over name
51+
tor.Title = torrSpec.DisplayName
5052
tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "")
5153
tor.Title = strings.ReplaceAll(tor.Title, "_", " ")
5254
tor.Title = strings.Trim(tor.Title, " ")
@@ -57,7 +59,71 @@ func addTorrent(c tele.Context, link string) error {
5759

5860
torr.SaveTorrentToDB(tor)
5961

60-
c.Bot().Edit(msg, "Торрент добавлен:\n<code>"+link+"</code>")
62+
if len(displayLabel) > 80 {
63+
displayLabel = displayLabel[:77] + "..."
64+
}
65+
_, _ = c.Bot().Edit(msg, fmt.Sprintf(tr(c.Sender().ID, "add_success"), displayLabel))
6166

6267
return nil
6368
}
69+
70+
func addTorrent(c tele.Context, link string) error {
71+
log.TLogln("tg add torrent", logHashOrTruncate(link))
72+
link = strings.ReplaceAll(link, "&amp;", "&")
73+
var torrSpec *torrent.TorrentSpec
74+
var err error
75+
if strings.HasPrefix(strings.ToLower(link), "torrs://") {
76+
torrSpec, _, err = utils.ParseTorrsHash(link)
77+
} else {
78+
torrSpec, err = utils.ParseLink(link)
79+
}
80+
if err != nil {
81+
log.TLogln("tg add parse err", err)
82+
return err
83+
}
84+
return addTorrentFromSpec(c, torrSpec, link)
85+
}
86+
87+
func addTorrentFromDocument(c tele.Context, doc *tele.Document) error {
88+
if doc == nil || doc.FileID == "" {
89+
return errors.New("no document")
90+
}
91+
reader, err := c.Bot().File(&doc.File)
92+
if err != nil {
93+
log.TLogln("tg add document getfile err", err)
94+
return err
95+
}
96+
defer func() { _ = reader.Close() }()
97+
data, err := io.ReadAll(reader)
98+
if err != nil {
99+
log.TLogln("tg add document read err", err)
100+
return err
101+
}
102+
torrSpec, err := utils.ParseFromBytes(data)
103+
if err != nil {
104+
log.TLogln("tg add document parse err", err)
105+
return err
106+
}
107+
displayLabel := doc.FileName
108+
if displayLabel == "" {
109+
displayLabel = ".torrent"
110+
}
111+
return addTorrentFromSpec(c, torrSpec, displayLabel)
112+
}
113+
114+
func cmdAdd(c tele.Context) error {
115+
uid := c.Sender().ID
116+
args := c.Args()
117+
if len(args) == 0 {
118+
return c.Send(tr(uid, "add_usage"))
119+
}
120+
link := strings.TrimSpace(strings.Join(args, " "))
121+
if link == "" {
122+
return c.Send(tr(uid, "add_no_link"))
123+
}
124+
err := addTorrent(c, link)
125+
if err != nil {
126+
return err
127+
}
128+
return list(c)
129+
}

server/tgbot/admin_common.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package tgbot
2+
3+
import (
4+
"server/tgbot/config"
5+
)
6+
7+
func isAdmin(userID int64) bool {
8+
if len(config.Cfg.WhiteIds) == 0 {
9+
return false
10+
}
11+
for _, id := range config.Cfg.WhiteIds {
12+
if id == userID {
13+
return true
14+
}
15+
}
16+
return false
17+
}

0 commit comments

Comments
 (0)