|
@@ -2,13 +2,11 @@ package tgbot
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
- "encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"errors"
|
|
"errors"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"html"
|
|
"html"
|
|
|
- "io"
|
|
|
|
|
- "net/http"
|
|
|
|
|
|
|
+ "net/url"
|
|
|
"slices"
|
|
"slices"
|
|
|
"strconv"
|
|
"strconv"
|
|
|
"strings"
|
|
"strings"
|
|
@@ -248,76 +246,42 @@ func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
|
|
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
|
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
|
|
|
|
|
-func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
|
|
|
|
- // Build the HTML sub page URL; we'll call it with header Accept to get raw content
|
|
|
|
|
- subURL, _, err := t.buildSubscriptionURLs(email)
|
|
|
|
|
|
|
+// clientSubLinks builds the subscription's links in-process for the host subURL
|
|
|
|
|
+// names; fetching subURL instead fails whenever that host does not resolve here.
|
|
|
|
|
+func (t *Tgbot) clientSubLinks(email, subURL string) ([]string, error) {
|
|
|
|
|
+ u, err := url.Parse(subURL)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ return nil, err
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Try to fetch raw subscription links. Prefer plain text response.
|
|
|
|
|
- req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subURL, nil)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ _, client, err := t.inboundService.GetClientByEmail(email)
|
|
|
|
|
+ if err != nil || client == nil {
|
|
|
|
|
+ return nil, errors.New("client not found")
|
|
|
}
|
|
}
|
|
|
- // Force plain text to avoid HTML page; controller respects Accept header
|
|
|
|
|
- req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
|
|
|
|
-
|
|
|
|
|
- // Use optimized client with connection pooling
|
|
|
|
|
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
|
|
|
- defer cancel()
|
|
|
|
|
- req = req.WithContext(ctx)
|
|
|
|
|
|
|
+ return t.inboundService.GetSubLinks(u.Hostname(), client.SubID)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- resp, err := optimizedHTTPClient.Do(req)
|
|
|
|
|
|
|
+// sendClientIndividualLinks sends the subscription's individual links to the user
|
|
|
|
|
+func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
|
|
|
|
+ subURL, _, err := t.buildSubscriptionURLs(email)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- defer resp.Body.Close()
|
|
|
|
|
-
|
|
|
|
|
- bodyBytes, err := io.ReadAll(resp.Body)
|
|
|
|
|
|
|
+ links, err := t.clientSubLinks(email, subURL)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // If service is configured to encode (Base64), decode it
|
|
|
|
|
- encoded, _ := t.settingService.GetSubEncrypt()
|
|
|
|
|
- var content string
|
|
|
|
|
- if encoded {
|
|
|
|
|
- decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- // fallback to raw text
|
|
|
|
|
- content = string(bodyBytes)
|
|
|
|
|
- } else {
|
|
|
|
|
- content = string(decoded)
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- content = string(bodyBytes)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Normalize line endings and trim
|
|
|
|
|
- lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
|
|
|
|
- var cleaned []string
|
|
|
|
|
- for _, l := range lines {
|
|
|
|
|
- l = strings.TrimSpace(l)
|
|
|
|
|
- if l != "" {
|
|
|
|
|
- cleaned = append(cleaned, l)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if len(cleaned) == 0 {
|
|
|
|
|
|
|
+ if len(links) == 0 {
|
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
|
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Send in chunks to respect message length; use monospace formatting
|
|
// Send in chunks to respect message length; use monospace formatting
|
|
|
const maxPerMessage = 50
|
|
const maxPerMessage = 50
|
|
|
- for i := 0; i < len(cleaned); i += maxPerMessage {
|
|
|
|
|
- j := min(i+maxPerMessage, len(cleaned))
|
|
|
|
|
- chunk := cleaned[i:j]
|
|
|
|
|
|
|
+ for i := 0; i < len(links); i += maxPerMessage {
|
|
|
|
|
+ j := min(i+maxPerMessage, len(links))
|
|
|
|
|
+ chunk := links[i:j]
|
|
|
var msg strings.Builder
|
|
var msg strings.Builder
|
|
|
msg.WriteString(t.I18nBot("subscription.individualLinks"))
|
|
msg.WriteString(t.I18nBot("subscription.individualLinks"))
|
|
|
msg.WriteString(":\r\n")
|
|
msg.WriteString(":\r\n")
|
|
@@ -375,51 +339,20 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Also generate a few individual links' QRs (first up to 5)
|
|
// Also generate a few individual links' QRs (first up to 5)
|
|
|
- subPageURL := subURL
|
|
|
|
|
- req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, subPageURL, nil)
|
|
|
|
|
- if err == nil {
|
|
|
|
|
- req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
|
|
|
|
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
|
|
|
- defer cancel()
|
|
|
|
|
- req = req.WithContext(ctx)
|
|
|
|
|
- if resp, err := optimizedHTTPClient.Do(req); err == nil {
|
|
|
|
|
- body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
- _ = resp.Body.Close()
|
|
|
|
|
- encoded, _ := t.settingService.GetSubEncrypt()
|
|
|
|
|
- var content string
|
|
|
|
|
- if encoded {
|
|
|
|
|
- if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {
|
|
|
|
|
- content = string(dec)
|
|
|
|
|
- } else {
|
|
|
|
|
- content = string(body)
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- content = string(body)
|
|
|
|
|
- }
|
|
|
|
|
- lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
|
|
|
|
- var cleaned []string
|
|
|
|
|
- for _, l := range lines {
|
|
|
|
|
- l = strings.TrimSpace(l)
|
|
|
|
|
- if l != "" {
|
|
|
|
|
- cleaned = append(cleaned, l)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if len(cleaned) > 0 {
|
|
|
|
|
- max := min(len(cleaned), 5)
|
|
|
|
|
- for i := range max {
|
|
|
|
|
- if png, err := createQR(cleaned[i], 320); err == nil {
|
|
|
|
|
- // Use the email as filename for individual link QR
|
|
|
|
|
- filename := email + ".png"
|
|
|
|
|
- document := tu.Document(
|
|
|
|
|
- tu.ID(chatId),
|
|
|
|
|
- tu.FileFromBytes(png, filename),
|
|
|
|
|
- )
|
|
|
|
|
- _, _ = bot.SendDocument(context.Background(), document)
|
|
|
|
|
- // Reduced delay for better performance
|
|
|
|
|
- if i < max-1 { // Only delay between documents, not after the last one
|
|
|
|
|
- time.Sleep(50 * time.Millisecond)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if links, err := t.clientSubLinks(email, subURL); err == nil {
|
|
|
|
|
+ max := min(len(links), 5)
|
|
|
|
|
+ for i := range max {
|
|
|
|
|
+ if png, err := createQR(links[i], 320); err == nil {
|
|
|
|
|
+ // Use the email as filename for individual link QR
|
|
|
|
|
+ filename := email + ".png"
|
|
|
|
|
+ document := tu.Document(
|
|
|
|
|
+ tu.ID(chatId),
|
|
|
|
|
+ tu.FileFromBytes(png, filename),
|
|
|
|
|
+ )
|
|
|
|
|
+ _, _ = bot.SendDocument(context.Background(), document)
|
|
|
|
|
+ // Reduced delay for better performance
|
|
|
|
|
+ if i < max-1 { // Only delay between documents, not after the last one
|
|
|
|
|
+ time.Sleep(50 * time.Millisecond)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|