Skip to content

Commit 6dc3929

Browse files
authored
Merge pull request #210 from rezatg/master
Refactor: Optimize Auth and Scrape Logic for Better Readability and Robustness
2 parents 7c5c233 + 893d4f5 commit 6dc3929

File tree

1 file changed

+130
-98
lines changed

1 file changed

+130
-98
lines changed

telegram/auth.go

Lines changed: 130 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package telegram
44

55
import (
6+
"bufio"
7+
"context"
68
"crypto/rand"
79
"encoding/base64"
810
"encoding/json"
@@ -11,6 +13,7 @@ import (
1113
"math"
1214
"net/http"
1315
"net/url"
16+
"os"
1417
"regexp"
1518
"strconv"
1619
"strings"
@@ -28,6 +31,8 @@ func (c *Client) ConnectBot(botToken string) error {
2831
return c.LoginBot(botToken)
2932
}
3033

34+
const maxRetries = 3
35+
3136
var (
3237
botTokenRegex = regexp.MustCompile(`^\d+:[\w\d_-]+$`)
3338
phoneRegex = regexp.MustCompile(`^\+?\d+$`)
@@ -38,34 +43,30 @@ func (c *Client) AuthPrompt() error {
3843
if au, _ := c.IsAuthorized(); au {
3944
return nil
4045
}
41-
var input string
42-
maxRetries := 3
43-
for i := 0; i < maxRetries; i++ {
44-
fmt.Printf("Enter phone number (with country code [+42xxx]) or bot token: ")
45-
fmt.Scanln(&input)
46-
if input != "" {
47-
if botTokenRegex.MatchString(input) {
48-
err := c.LoginBot(input)
49-
if err != nil {
50-
return err
51-
}
52-
} else {
53-
if phoneRegex.MatchString(strings.TrimSpace(input)) {
54-
_, err := c.Login(input)
55-
if err != nil {
56-
return err
57-
}
58-
} else {
59-
fmt.Println("The input is not a valid phone number or bot token, try again [", i+1, "/", maxRetries, "]")
60-
continue
61-
}
62-
}
63-
break
64-
} else {
65-
fmt.Println("Invalid response, try again")
46+
47+
reader := bufio.NewReader(os.Stdin)
48+
for i := range maxRetries {
49+
fmt.Print("Enter phone number (with country code [+42xxx]) or bot token: ")
50+
input, err := reader.ReadString('\n')
51+
if err != nil {
52+
return fmt.Errorf("failed to read input: %w", err)
53+
}
54+
input = strings.TrimSpace(input)
55+
if input == "" {
56+
fmt.Printf("Invalid response, try again [%d/%d]\n", i+1, maxRetries)
57+
continue
58+
}
59+
60+
if botTokenRegex.MatchString(input) {
61+
return c.LoginBot(input)
62+
}
63+
if phoneRegex.MatchString(input) {
64+
_, err := c.Login(input)
65+
return err
6666
}
67+
fmt.Printf("The input is not a valid phone number or bot token, try again [%d/%d]\n", i+1, maxRetries)
6768
}
68-
return nil
69+
return fmt.Errorf("max retries exceeded for authentication")
6970
}
7071

7172
// Authorize client with bot token
@@ -288,139 +289,163 @@ type ScrapeConfig struct {
288289
}
289290

290291
func (c *Client) ScrapeAppConfig(config ...*ScrapeConfig) (int32, string, bool, error) {
291-
var conf = getVariadic(config, &ScrapeConfig{
292+
conf := getVariadic(config, &ScrapeConfig{
292293
CreateIfNotExists: true,
293294
})
294295

295296
if conf.PhoneNum == "" {
296-
fmt.Printf("Enter phone number (with country code [+1xxx]): ")
297-
fmt.Scanln(&conf.PhoneNum)
297+
reader := bufio.NewReader(os.Stdin)
298+
fmt.Print("Enter phone number (with country code [+1xxx]): ")
299+
input, err := reader.ReadString('\n')
300+
if err != nil {
301+
return 0, "", false, fmt.Errorf("failed to read phone number: %w", err)
302+
}
303+
conf.PhoneNum = strings.TrimSpace(input)
298304
}
299305

300306
if conf.WebCodeCallback == nil {
301307
conf.WebCodeCallback = func() (string, error) {
302-
fmt.Printf("Enter received web login code: ")
303-
var password string
304-
fmt.Scanln(&password)
305-
return password, nil
308+
reader := bufio.NewReader(os.Stdin)
309+
fmt.Print("Enter received web login code: ")
310+
311+
code, err := reader.ReadString('\n')
312+
if err != nil {
313+
return "", fmt.Errorf("failed to read web code: %w", err)
314+
}
315+
return strings.TrimSpace(code), nil
306316
}
307317
}
308318

309319
customClient := &http.Client{
310320
Timeout: time.Second * 10,
311321
}
312322

323+
// Send code request
313324
reqCode, err := http.NewRequest("POST", "https://my.telegram.org/auth/send_password", strings.NewReader("phone="+url.QueryEscape(conf.PhoneNum)))
314325
if err != nil {
315-
return 0, "", false, err
326+
return 0, "", false, fmt.Errorf("failed to create code request: %w", err)
316327
}
317-
318328
reqCode.Header.Set("Content-Type", "application/x-www-form-urlencoded")
319329

320330
respCode, err := customClient.Do(reqCode)
321-
if err != nil || respCode.StatusCode != 200 {
322-
return 0, "", false, err
331+
if err != nil {
332+
return 0, "", false, fmt.Errorf("failed to send code request: %w", err)
333+
}
334+
defer respCode.Body.Close()
335+
if respCode.StatusCode != 200 {
336+
return 0, "", false, fmt.Errorf("code request failed with status: %d", respCode.StatusCode)
323337
}
324338

325339
var result struct {
326340
RandomHash string `json:"random_hash"`
327341
}
328-
329342
if err := json.NewDecoder(respCode.Body).Decode(&result); err != nil {
330-
return 0, "", false, errors.Wrap(err, "Too many requests, try again later")
343+
return 0, "", false, errors.Wrap(err, "failed to decode response, too many requests?")
331344
}
332345

333346
code, err := conf.WebCodeCallback()
334347
if err != nil {
335-
return 0, "", false, err
348+
return 0, "", false, fmt.Errorf("failed to get web code: %w", err)
336349
}
337350

351+
// Login request
338352
reqLogin, err := http.NewRequest("POST", "https://my.telegram.org/auth/login", strings.NewReader("phone="+url.QueryEscape(conf.PhoneNum)+"&random_hash="+result.RandomHash+"&password="+url.QueryEscape(code)))
339353
if err != nil {
340-
return 0, "", false, err
354+
return 0, "", false, fmt.Errorf("failed to create login request: %w", err)
341355
}
342-
343356
reqLogin.Header.Set("Content-Type", "application/x-www-form-urlencoded")
344357

345358
respLogin, err := customClient.Do(reqLogin)
346-
if err != nil || respLogin.StatusCode != 200 {
347-
return 0, "", false, err
359+
if err != nil {
360+
return 0, "", false, fmt.Errorf("failed to send login request: %w", err)
361+
}
362+
defer respLogin.Body.Close()
363+
if respLogin.StatusCode != 200 {
364+
return 0, "", false, fmt.Errorf("login request failed with status: %d", respLogin.StatusCode)
348365
}
349366

350367
cookies := respLogin.Cookies()
351-
ALREDY_TRIED_CREATION := false
352-
353-
BackToAppsPage:
354-
reqScrape, err := http.NewRequest("GET", "https://my.telegram.org/apps", nil)
368+
appID, appHash, err := c.scrapeAppDetails(customClient, cookies, conf.CreateIfNotExists)
355369
if err != nil {
356370
return 0, "", false, err
357371
}
358372

373+
return appID, appHash, true, nil
374+
}
375+
376+
func (c *Client) scrapeAppDetails(client *http.Client, cookies []*http.Cookie, createIfNotExists bool) (int32, string, error) {
377+
appIDRegex := regexp.MustCompile(`<label for="app_id".*?>.*?</label>\s*<div.*?>\s*<span.*?><strong>(\d+)</strong></span>`)
378+
appHashRegex := regexp.MustCompile(`<label for="app_hash".*?>.*?</label>\s*<div.*?>\s*<span.*?>([a-fA-F0-9]+)</span>`)
379+
380+
reqScrape, err := http.NewRequest("GET", "https://my.telegram.org/apps", nil)
381+
if err != nil {
382+
return 0, "", fmt.Errorf("failed to create scrape request: %w", err)
383+
}
359384
for _, cookie := range cookies {
360385
reqScrape.AddCookie(cookie)
361386
}
362387

363-
respScrape, err := customClient.Do(reqScrape)
364-
if err != nil || respScrape.StatusCode != 200 {
365-
return 0, "", false, err
388+
respScrape, err := client.Do(reqScrape)
389+
if err != nil {
390+
return 0, "", fmt.Errorf("failed to send scrape request: %w", err)
391+
}
392+
defer respScrape.Body.Close()
393+
if respScrape.StatusCode != 200 {
394+
return 0, "", fmt.Errorf("scrape request failed with status: %d", respScrape.StatusCode)
366395
}
367396

368397
body, err := io.ReadAll(respScrape.Body)
369398
if err != nil {
370-
return 0, "", false, err
399+
return 0, "", fmt.Errorf("failed to read scrape response: %w", err)
371400
}
372401

373-
appIDRegex := regexp.MustCompile(`<label for="app_id".*?>.*?</label>\s*<div.*?>\s*<span.*?><strong>(\d+)</strong></span>`)
374-
appHashRegex := regexp.MustCompile(`<label for="app_hash".*?>.*?</label>\s*<div.*?>\s*<span.*?>([a-fA-F0-9]+)</span>`)
375-
376402
appID := appIDRegex.FindStringSubmatch(string(body))
377403
appHash := appHashRegex.FindStringSubmatch(string(body))
378404

379-
if len(appID) < 2 || len(appHash) < 2 || strings.Contains(string(body), "Create new application") && !ALREDY_TRIED_CREATION {
380-
ALREDY_TRIED_CREATION = true
381-
// assume app is not created, create app
382-
hiddenHashRegex := regexp.MustCompile(`<input type="hidden" name="hash" value="([a-fA-F0-9]+)"\/>`)
383-
hiddenHash := hiddenHashRegex.FindStringSubmatch(string(body))
384-
if len(hiddenHash) < 2 {
385-
return 0, "", false, errors.New("creation hash not found, try manual creation")
386-
}
387-
388-
appRandomSuffix := make([]byte, 8)
389-
_, err := rand.Read(appRandomSuffix)
390-
391-
if err != nil {
392-
return 0, "", false, err
393-
}
405+
if len(appID) < 2 || len(appHash) < 2 {
406+
if !createIfNotExists || strings.Contains(string(body), "Create new application") {
407+
hiddenHashRegex := regexp.MustCompile(`<input type="hidden" name="hash" value="([a-fA-F0-9]+)"\/>`)
408+
hiddenHash := hiddenHashRegex.FindStringSubmatch(string(body))
409+
if len(hiddenHash) < 2 {
410+
return 0, "", errors.New("creation hash not found, try manual creation")
411+
}
394412

395-
reqCreateApp, err := http.NewRequest("POST", "https://my.telegram.org/apps/create", strings.NewReader("hash="+hiddenHash[1]+"&app_title=AppForGogram"+string(appRandomSuffix)+"&app_shortname=gogramapp"+string(appRandomSuffix)+"&app_platform=android&app_url=https%3A%2F%2Fgogram.vercel.app&app_desc=ForGoGram"+string(appRandomSuffix)))
396-
if err != nil {
397-
return 0, "", false, err
398-
}
413+
appRandomSuffix := make([]byte, 8)
414+
if _, err := rand.Read(appRandomSuffix); err != nil {
415+
return 0, "", fmt.Errorf("failed to generate random suffix: %w", err)
416+
}
399417

400-
reqCreateApp.Header.Set("Content-Type", "application/x-www-form-urlencoded")
418+
reqCreateApp, err := http.NewRequest("POST", "https://my.telegram.org/apps/create", strings.NewReader("hash="+hiddenHash[1]+"&app_title=AppForGogram"+string(appRandomSuffix)+"&app_shortname=gogramapp"+string(appRandomSuffix)+"&app_platform=android&app_url=https%3A%2F%2Fgogram.vercel.app&app_desc=ForGoGram"+string(appRandomSuffix)))
419+
if err != nil {
420+
return 0, "", fmt.Errorf("failed to create app creation request: %w", err)
421+
}
422+
reqCreateApp.Header.Set("Content-Type", "application/x-www-form-urlencoded")
423+
for _, cookie := range cookies {
424+
reqCreateApp.AddCookie(cookie)
425+
}
401426

402-
for _, cookie := range cookies {
403-
reqCreateApp.AddCookie(cookie)
404-
}
427+
respCreateApp, err := client.Do(reqCreateApp)
428+
if err != nil {
429+
return 0, "", fmt.Errorf("failed to send app creation request: %w", err)
430+
}
431+
defer respCreateApp.Body.Close()
432+
if respCreateApp.StatusCode != 200 {
433+
return 0, "", fmt.Errorf("app creation request failed with status: %d", respCreateApp.StatusCode)
434+
}
405435

406-
respCreateApp, err := customClient.Do(reqCreateApp)
407-
if err != nil || respCreateApp.StatusCode != 200 {
408-
return 0, "", false, err
436+
return c.scrapeAppDetails(client, cookies, false)
409437
}
410-
411-
goto BackToAppsPage
438+
return 0, "", errors.New("failed to scrape app ID or hash")
412439
}
413440

414441
appIdNum, err := strconv.Atoi(appID[1])
415442
if err != nil {
416-
return 0, "", false, err
443+
return 0, "", fmt.Errorf("failed to parse app ID: %w", err)
444+
} else if appIdNum > math.MaxInt32 || appIdNum < math.MinInt32 {
445+
return 0, "", errors.New("app ID is out of range")
417446
}
418447

419-
if appIdNum > math.MaxInt32 || appIdNum < math.MinInt32 {
420-
return 0, "", false, errors.New("app id is out of range")
421-
}
422-
423-
return int32(appIdNum), appHash[1], true, nil
448+
return int32(appIdNum), appHash[1], nil
424449
}
425450

426451
func (c *Client) AcceptTOS() (bool, error) {
@@ -432,11 +457,13 @@ func (c *Client) AcceptTOS() (bool, error) {
432457
case *HelpTermsOfServiceUpdateObj:
433458
fmt.Println(tos.TermsOfService.Text)
434459
fmt.Println("Do you accept the TOS? (y/n)")
460+
435461
var input string
436462
fmt.Scanln(&input)
437463
if input != "y" {
438464
return false, nil
439465
}
466+
440467
return c.HelpAcceptTermsOfService(tos.TermsOfService.ID)
441468
default:
442469
return false, nil
@@ -557,14 +584,19 @@ func (q *QrToken) Recreate() (*QrToken, error) {
557584
return q, err
558585
}
559586

560-
func (q *QrToken) Wait(timeout ...int32) error {
561-
const def int32 = 600 // 10 minutes
562-
q.Timeout = getVariadic(timeout, def)
587+
func (q *QrToken) Wait(ctx context.Context, timeout ...int32) error {
588+
const defaultTimeout int32 = 600 // 10 minutes
589+
q.Timeout = getVariadic(timeout, defaultTimeout)
590+
591+
ctx, cancel := context.WithTimeout(ctx, time.Duration(q.Timeout)*time.Second)
592+
defer cancel()
593+
563594
ch := make(chan int)
564595
ev := q.client.AddRawHandler(&UpdateLoginToken{}, func(update Update, client *Client) error {
565596
ch <- 1
566597
return nil
567598
})
599+
568600
select {
569601
case <-ch:
570602
go q.client.removeHandle(ev)
@@ -593,20 +625,19 @@ func (q *QrToken) Wait(timeout ...int32) error {
593625
}
594626
}
595627
return nil
596-
case <-time.After(time.Duration(q.Timeout) * time.Second):
628+
case <-ctx.Done():
597629
go q.client.removeHandle(ev)
598630
return errors.New("qr login timed out")
599631
}
600632
}
601633

602634
func (c *Client) QRLogin(IgnoreIDs ...int64) (*QrToken, error) {
603635
// Get QR code
604-
var ignoreIDs []int64
605-
ignoreIDs = append(ignoreIDs, IgnoreIDs...)
606-
qr, err := c.AuthExportLoginToken(c.AppID(), c.AppHash(), ignoreIDs)
636+
qr, err := c.AuthExportLoginToken(c.AppID(), c.AppHash(), IgnoreIDs)
607637
if err != nil {
608-
return nil, err
638+
return nil, fmt.Errorf("failed to export QR token: %v", err)
609639
}
640+
610641
var (
611642
qrToken []byte
612643
expiresIn int32 = 60
@@ -619,14 +650,15 @@ func (c *Client) QRLogin(IgnoreIDs ...int64) (*QrToken, error) {
619650
qrToken = qr.Token
620651
expiresIn = qr.Expires
621652
}
653+
622654
// Get QR code URL
623655
qrURL := base64.RawURLEncoding.EncodeToString(qrToken)
624656
return &QrToken{
625657
Token: qrToken,
626658
Url: fmt.Sprintf("tg://login?token=%s", qrURL),
627659
ExpiresIn: expiresIn,
628660
client: c,
629-
IgnoredIDs: ignoreIDs,
661+
IgnoredIDs: IgnoreIDs,
630662
}, nil
631663
}
632664

0 commit comments

Comments
 (0)