Initial commit: docker compose config
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled

This commit is contained in:
2026-05-30 12:07:11 +00:00
commit 616c6b1c62
381 changed files with 55145 additions and 0 deletions
+363
View File
@@ -0,0 +1,363 @@
package tgbot
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"golang.org/x/net/proxy"
tele "gopkg.in/telebot.v4"
"gopkg.in/telebot.v4/middleware"
"server/log"
"server/tgbot/config"
up "server/tgbot/upload"
)
func newTelegramHTTPClient() *http.Client {
const timeout = 5 * time.Minute
trimmed := strings.TrimSpace(config.Cfg.Socks5)
if trimmed == "" {
return &http.Client{Timeout: timeout}
}
raw := trimmed
if !strings.Contains(raw, "://") {
raw = "socks5://" + raw
}
u, err := url.Parse(raw)
if err != nil {
log.TLogln("tg cfg Socks5 parse err, using direct", err)
return &http.Client{Timeout: timeout}
}
if u.Scheme != "socks5" {
log.TLogln("tg cfg Socks5: only socks5 is supported, got", u.Scheme)
return &http.Client{Timeout: timeout}
}
proxyHost := u.Host
if proxyHost == "" {
log.TLogln("tg cfg Socks5: empty host, using direct")
return &http.Client{Timeout: timeout}
}
var auth *proxy.Auth
if u.User != nil {
pw, _ := u.User.Password()
auth = &proxy.Auth{User: u.User.Username(), Password: pw}
}
socksDial, err := proxy.SOCKS5("tcp", proxyHost, auth, proxy.Direct)
if err != nil {
log.TLogln("tg socks5 dialer err, using direct", err)
return &http.Client{Timeout: timeout}
}
log.TLogln("tg using SOCKS5 proxy", proxyHost)
transport := &http.Transport{
Proxy: nil, // respect explicit socks only, not HTTP_PROXY, for this client
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
_ = ctx
return socksDial.Dial(network, address)
},
}
return &http.Client{Transport: transport, Timeout: timeout}
}
func Start(token string) error {
config.LoadConfig()
loadUserLangs()
pref := tele.Settings{
URL: config.Cfg.HostTG,
Token: token,
Poller: &tele.LongPoller{Timeout: 5 * time.Minute},
ParseMode: tele.ModeHTML,
Client: newTelegramHTTPClient(),
}
log.TLogln("tg bot starting")
b, err := tele.NewBot(pref)
if err != nil {
log.TLogln("tg bot start err", err)
return err
}
up.TrFunc = tr
up.EscapeFunc = escapeHtml
if err := b.SetCommands([]tele.Command{
{Text: "help", Description: "Help and user ID"},
{Text: "start", Description: "Start bot"},
{Text: "list", Description: "List torrents"},
{Text: "add", Description: "Add torrent"},
{Text: "search", Description: "Search all (RuTor+Torznab)"},
{Text: "rutor", Description: "Search RuTor"},
{Text: "torznab", Description: "Search Torznab"},
{Text: "remove", Description: "Remove torrent"},
{Text: "status", Description: "Torrent status"},
{Text: "link", Description: "Stream link"},
{Text: "m3u", Description: "M3U playlist"},
{Text: "preload", Description: "Preload file"},
{Text: "queue", Description: "Upload queue status"},
{Text: "server", Description: "Server info"},
{Text: "stats", Description: "Summary statistics"},
{Text: "stat", Description: "Detailed status"},
{Text: "snake", Description: "Cache visualization"},
{Text: "clear", Description: "Remove all torrents"},
{Text: "hash", Description: "Show hashes"},
{Text: "export", Description: "Export torrents"},
{Text: "import", Description: "Import torrents"},
{Text: "categories", Description: "List categories"},
{Text: "lang", Description: "Set language RU|EN"},
}); err != nil {
log.TLogln("tg setcmd err", err)
}
if len(config.Cfg.WhiteIds) > 0 {
b.Use(middleware.Whitelist(config.Cfg.WhiteIds...))
}
if len(config.Cfg.BlackIds) > 0 {
b.Use(middleware.Blacklist(config.Cfg.BlackIds...))
}
b.Use(func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if c.Sender() == nil {
return nil
}
if c.Message() != nil && c.Message().Text != "" {
cmd := logSafeStr(c.Message().Text, 60)
log.TLogln("tg cmd", logUser(c.Sender()), cmd)
}
err := next(c)
if err != nil {
log.TLogln("tg cmd err", logUser(c.Sender()), err)
}
return err
}
})
b.Handle("help", help)
b.Handle("Help", help)
b.Handle("/help", help)
b.Handle("/Help", help)
b.Handle("/start", help)
b.Handle("/id", help)
b.Handle("/list", list)
b.Handle("/clear", clear)
b.Handle("/add", cmdAdd)
b.Handle("/remove", cmdRemove)
b.Handle("/drop", cmdDrop)
b.Handle("/status", cmdStatus)
b.Handle("/server", cmdServer)
b.Handle("/link", cmdLink)
b.Handle("/play", cmdLink)
b.Handle("/cache", cmdCache)
b.Handle("/m3u", cmdM3u)
b.Handle("/m3uall", cmdM3uAll)
b.Handle("/search", cmdSearch)
b.Handle("/rutor", cmdSearchRutor)
b.Handle("/torznab", cmdTorznab)
b.Handle("/preload", cmdPreload)
b.Handle("/queue", up.ShowQueue)
b.Handle("/set", cmdSet)
b.Handle("/hash", cmdHash)
b.Handle("/export", cmdExport)
b.Handle("/import", cmdImport)
b.Handle("/categories", cmdCategories)
b.Handle("/echo", cmdEcho)
b.Handle("/db", cmdDb)
b.Handle("/viewed", cmdViewed)
b.Handle("/ffp", cmdFfp)
b.Handle("/speedtest", cmdSpeedtest)
b.Handle("/shutdown", adminOnly(cmdShutdown))
b.Handle("/settings", adminOnly(cmdSettings))
b.Handle("/preset", adminOnly(cmdPreset))
b.Handle("/lang", cmdLang)
b.Handle("/stats", cmdStats)
b.Handle("/stat", cmdStat)
b.Handle("/snake", cmdSnake)
b.Handle(tele.OnDocument, func(c tele.Context) error {
if c.Message() == nil {
return nil
}
doc := c.Message().Document
if doc == nil {
return nil
}
lowerName := strings.ToLower(doc.FileName)
isTorrent := strings.HasSuffix(lowerName, ".torrent") ||
strings.Contains(strings.ToLower(doc.MIME), "bittorrent")
if isTorrent {
err := addTorrentFromDocument(c, doc)
if err != nil {
return err
}
return list(c)
}
return nil
})
b.Handle(tele.OnText, func(c tele.Context) error {
txt := c.Text()
if handleSettingsInputReply(c) {
return nil
}
lower := strings.ToLower(txt)
if strings.HasPrefix(lower, "magnet:") || strings.HasPrefix(lower, "torrs://") ||
strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") ||
isHash(txt) {
err := addTorrent(c, txt)
if err != nil {
return err
}
return list(c)
} else if c.Message().ReplyTo != nil && c.Message().ReplyTo.ReplyMarkup != nil && len(c.Message().ReplyTo.ReplyMarkup.InlineKeyboard) > 0 {
var hash string
for _, row := range c.Message().ReplyTo.ReplyMarkup.InlineKeyboard {
for _, btn := range row {
if btn.Data == "" {
continue
}
if idx := strings.Index(btn.Data, "all|"); idx >= 0 {
h := btn.Data[idx+4:]
if len(h) >= 40 && isHash(h[:40]) {
hash = h[:40]
} else if isHash(h) {
hash = h
}
} else if isHash(btn.Data) {
hash = btn.Data
}
if hash != "" {
break
}
}
if hash != "" {
break
}
}
if hash != "" {
from, to, err := ParseRange(c.Sender().ID, c.Message().Text)
if err != nil {
_ = c.Send(tr(c.Sender().ID, "range_error"))
return err
}
up.AddRange(c, hash, from, to)
}
return nil
} else {
return c.Send(tr(c.Sender().ID, "add_magnet"))
}
})
b.Handle(tele.OnQuery, handleInlineQuery)
b.Handle(tele.OnCallback, func(c tele.Context) error {
args := c.Args()
if len(args) > 0 {
cbInfo := strings.TrimPrefix(args[0], "\f")
if len(args) >= 2 {
cbInfo += " " + args[1]
}
cbInfo = logSafeStr(cbInfo, 80)
log.TLogln("tg cb", logUser(c.Sender()), cbInfo)
}
err := handleCallback(c)
if err != nil && len(args) > 0 {
log.TLogln("tg cb err", logUser(c.Sender()), logSafeStr(args[0], 40), err)
}
return err
})
up.Start()
go b.Start()
return nil
}
func help(c tele.Context) error {
uid := c.Sender().ID
id := strconv.FormatInt(uid, 10)
var arr []string
if c.Sender().Username != "" {
arr = append(arr, c.Sender().Username)
}
if c.Sender().FirstName != "" {
arr = append(arr, c.Sender().FirstName)
}
if c.Sender().LastName != "" {
arr = append(arr, c.Sender().LastName)
}
msg := "🤖 <b>" + tr(uid, "help") + "</b>\n\n"
msg += "📋 <b>" + tr(uid, "help_main") + "</b>\n"
msg += " • /help — " + tr(uid, "help_help") + "\n"
msg += " • " + tr(uid, "help_list") + "\n"
msg += " • " + tr(uid, "help_clear") + "\n"
msg += " • " + tr(uid, "help_add") + "\n"
msg += " • " + tr(uid, "help_hash") + "\n"
msg += " • /stats, /stat — " + tr(uid, "help_stats") + ", " + tr(uid, "help_stat") + "\n\n"
msg += "🎛 <b>" + tr(uid, "help_manage") + "</b> " + tr(uid, "help_manage_desc") + "\n"
msg += " • " + tr(uid, "help_remove") + "\n"
msg += " • " + tr(uid, "help_links") + "\n\n"
msg += "🔍 <b>" + tr(uid, "help_search") + "</b> " + tr(uid, "help_search_desc") + "\n"
msg += " • " + tr(uid, "help_search_cmd") + "\n\n"
msg += "📦 <b>" + tr(uid, "help_export_import") + "</b>\n"
msg += " • " + tr(uid, "help_export") + "\n"
msg += " • " + tr(uid, "help_import") + "\n\n"
msg += "📁 <b>" + tr(uid, "help_categories_section") + "</b>\n"
msg += " • " + tr(uid, "help_categories") + "\n\n"
msg += "🖥 <b>" + tr(uid, "help_server") + "</b>\n"
msg += " • " + tr(uid, "help_server_cmd") + "\n"
msg += " • " + tr(uid, "help_echo") + "\n"
msg += " • " + tr(uid, "help_db") + "\n\n"
msg += "⚙️ <b>" + tr(uid, "help_other") + "</b>\n"
msg += " • " + tr(uid, "help_other_cmd") + "\n"
msg += " • " + tr(uid, "help_lang") + "\n"
msg += " • " + tr(uid, "help_admin") + "\n\n"
msg += "👤 " + tr(uid, "help_id") + ": <code>" + id + "</code>"
if len(arr) > 0 {
msg += " • " + strings.Join(arr, ", ")
}
return c.Send(msg)
}
func isHash(txt string) bool {
if len(txt) == 40 {
for _, c := range strings.ToLower(txt) {
switch c {
case 'a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
return false
}
}
return true
}
return false
}
func ParseRange(userID int64, rng string) (int, int, error) {
parts := strings.Split(rng, "-")
if len(parts) != 2 {
return -1, -1, errors.New(tr(userID, "parse_range_err"))
}
num1, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
if err1 != nil {
return -1, -1, err1
}
num2, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
if err2 != nil {
return -1, -1, err2
}
if num1 < 1 || num2 < 1 || num1 > num2 {
return -1, -1, errors.New(tr(userID, "parse_range_err"))
}
return num1, num2, nil
}