33package telegram
44
55import (
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+
3136var (
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
290291func (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
426451func (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
602634func (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