616c6b1c62
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
364 lines
10 KiB
Go
364 lines
10 KiB
Go
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
|
|
}
|