77using NLog ;
88using UniversalDownloaderPlatform . Common . Interfaces ;
99using PuppeteerSharp ;
10+ using PuppeteerSharp . Input ;
1011using UniversalDownloaderPlatform . PuppeteerEngine . Interfaces ;
1112using UniversalDownloaderPlatform . PuppeteerEngine . Interfaces . Wrappers . Browser ;
1213using UniversalDownloaderPlatform . Common . Interfaces . Models ;
@@ -23,7 +24,7 @@ public class PuppeteerCookieRetriever : ICookieRetriever, IDisposable
2324 private IPuppeteerSettings _settings ;
2425 private bool _isHeadlessBrowser ;
2526 private bool _isRemoteBrowser ;
26-
27+ private bool _shouldTryAutoLogin ;
2728
2829 /// <summary>
2930 /// Create new instance of PuppeteerCookieRetriever
@@ -40,6 +41,7 @@ public PuppeteerCookieRetriever()
4041 public Task BeforeStart ( IUniversalDownloaderPlatformSettings settings )
4142 {
4243 _settings = settings as IPuppeteerSettings ;
44+ _shouldTryAutoLogin = ! string . IsNullOrWhiteSpace ( _settings . LoginEmail ) && ! string . IsNullOrWhiteSpace ( _settings . LoginPassword ) ;
4345
4446 if ( _settings . RemoteBrowserAddress != null )
4547 {
@@ -83,12 +85,12 @@ protected virtual async Task Login()
8385 if ( ! await IsLoggedIn ( response ) )
8486 {
8587 _logger . Debug ( "We are NOT logged in, opening login page" ) ;
86- if ( _isRemoteBrowser )
88+ if ( _isRemoteBrowser && ! _shouldTryAutoLogin )
8789 {
8890 await page . CloseAsync ( ) ;
8991 throw new Exception ( "You are not logged in into your account in remote browser. Please login and restart application." ) ;
9092 }
91- if ( _puppeteerEngine . IsHeadless )
93+ if ( _puppeteerEngine . IsHeadless && ! _shouldTryAutoLogin )
9294 {
9395 _logger . Debug ( "Puppeteer is in headless mode, restarting in full mode" ) ;
9496 browser = await RestartBrowser ( false ) ;
@@ -100,7 +102,16 @@ protected virtual async Task Login()
100102 page . GoToAsync ( _settings . LoginPageAddress , null ) ;
101103#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
102104
103- await page . WaitForRequestAsync ( request => { return request . Url . Contains ( _settings . LoginCheckAddress ) ; } ) ;
105+ if ( _shouldTryAutoLogin )
106+ {
107+ _logger . Debug ( "Credentials were supplied, attempting automatic Patreon login" ) ;
108+ await TryAutoLogin ( page ) ;
109+ }
110+ else
111+ {
112+ _logger . Debug ( "Waiting for user to log in Patreon manually" ) ;
113+ await page . WaitForRequestAsync ( request => { return request . Url . Contains ( _settings . LoginCheckAddress ) ; } ) ;
114+ }
104115 }
105116 else
106117 {
@@ -118,6 +129,93 @@ protected virtual async Task Login()
118129 await page . CloseAsync ( ) ;
119130 }
120131
132+ private async Task TryAutoLogin ( IWebPage page )
133+ {
134+ try
135+ {
136+ await page . WaitForNetworkIdleAsync ( new WaitForNetworkIdleOptions ( )
137+ {
138+ // The hanging requests are:
139+ // https://accounts.google.com/gsi/client
140+ // https://www.facebook.com/x/oauth/status
141+ // https://www.google.com/recaptcha/enterprise/webworker.js
142+ Concurrency = 3 ,
143+ IdleTime = 1000 ,
144+ Timeout = 10000 ,
145+ } ) ;
146+ }
147+ catch ( WaitTaskTimeoutException ) // In case there are other hanging requests (they seem to appear randomly)
148+ {
149+ _logger . Debug ( "Waiting for network idle timeout; proceed anyway and hope for the best" ) ;
150+ }
151+
152+ IWebResponse response = await EnterAndSubmit ( page , "input[aria-label=\" Email\" ]" , _settings . LoginEmail ) ;
153+ if ( ( await response . TextAsync ( ) ) . Contains ( "\" next_auth_step\" :\" signup\" " ) )
154+ {
155+ throw new Exception ( "There does not exist an account with the provided email" ) ;
156+ }
157+ await EnterAndSubmit ( page , "input[aria-label=\" Password\" ]" , _settings . LoginPassword ) ;
158+
159+ // Not sure why this is needed, but otherwise GoToAsync will throw PuppeteerSharp.NavigationException: net::ERR_ABORTED
160+ await page . CloseAsync ( ) ;
161+ }
162+
163+ private async Task < IWebResponse > EnterAndSubmit ( IWebPage page , string selector , string text )
164+ {
165+ const string submitSelector = "button[type=\" submit\" ][aria-disabled=\" false\" ]" ;
166+
167+ await page . WaitForSelectorAsync ( selector , new WaitForSelectorOptions ( ) { Timeout = 10000 } ) ;
168+ _logger . Debug ( $ "Found { selector } , entering information") ;
169+ await Task . Delay ( 300 ) ;
170+
171+ int retry ;
172+ for ( retry = 0 ; retry < 5 ; retry ++ )
173+ {
174+ await page . TypeAsync ( selector , text , new TypeOptions ( ) { Delay = 50 } ) ;
175+ try
176+ {
177+ await page . WaitForSelectorAsync ( submitSelector , new WaitForSelectorOptions ( ) { Timeout = 3000 } ) ;
178+ }
179+ catch ( WaitTaskTimeoutException )
180+ {
181+ _logger . Debug ( $ "Submit button did not appear; retrying { retry } /5") ;
182+ await page . ClickAsync ( selector , new ClickOptions ( )
183+ {
184+ Count = 3 , // hopefully select all text in the field
185+ Delay = 50 ,
186+ OffSet = new Offset ( 10 , 10 )
187+ } ) ;
188+ continue ;
189+ }
190+ break ;
191+ }
192+ if ( retry == 5 )
193+ {
194+ throw new Exception ( "Cannot find the submit button after 5 tries" ) ;
195+ }
196+
197+ await Task . Delay ( 300 ) ;
198+ await page . ClickAsync ( submitSelector ) ;
199+
200+ IWebResponse authResponse = await page . WaitForResponseAsync (
201+ response => { return response . Url . Contains ( _settings . AuthAddress ) ; } ,
202+ new WaitForOptions ( ) { Timeout = 10000 }
203+ ) ;
204+ switch ( authResponse . Status )
205+ {
206+ case HttpStatusCode . OK :
207+ return authResponse ;
208+ case HttpStatusCode . BadRequest :
209+ throw new Exception ( $ "Auth returned non-OK code: { authResponse . Status } ; you probably provided an invalid email") ;
210+ case HttpStatusCode . TooManyRequests :
211+ throw new Exception ( $ "Auth returned non-OK code: { authResponse . Status } ; you probably tried logging in for too many times") ;
212+ case HttpStatusCode . Forbidden :
213+ throw new Exception ( $ "Auth returned non-OK code: { authResponse . Status } ; you either provided a wrong password or are blocked") ;
214+ default :
215+ throw new Exception ( $ "Auth returned non-OK code: { authResponse . Status } ") ;
216+ }
217+ }
218+
121219 /// <summary>
122220 /// Perform check if the received response contains data which can be used to assume that we are logged in
123221 /// </summary>
0 commit comments