From 0c5a2d73602d4bbb56a71bc6ebd670e3e4e6ca05 Mon Sep 17 00:00:00 2001 From: perennial Date: Sun, 13 Oct 2024 18:34:38 +1100 Subject: [PATCH] debug wip --- assets/css/bootstrap-style.css | 12 ++++ assets/css/bootstrap-style.scss | 11 ++++ assets/views/user.jet.html | 36 ++++++++++- core/requests.go | 106 +++++++++++++++++--------------- core/user.go | 2 + i18n/locale/en/code.json | 3 +- i18n/locale/en/template.json | 3 + server/audit/audit_init.go | 11 +++- server/audit/audit_log.go | 8 +-- server/routes/actions.go | 27 ++++++++ 10 files changed, 158 insertions(+), 61 deletions(-) diff --git a/assets/css/bootstrap-style.css b/assets/css/bootstrap-style.css index 017b772..81dcd26 100644 --- a/assets/css/bootstrap-style.css +++ b/assets/css/bootstrap-style.css @@ -14139,6 +14139,18 @@ h6, .h6 { display: inline; } +.follow-spinner { + display: none; +} + +.htmx-request .follow-spinner { + display: inline; +} + +.htmx-request.follow-spinner { + display: inline; +} + @media (scripting: none) { .js-required { display: none !important; diff --git a/assets/css/bootstrap-style.scss b/assets/css/bootstrap-style.scss index ba4332b..11002f3 100644 --- a/assets/css/bootstrap-style.scss +++ b/assets/css/bootstrap-style.scss @@ -317,6 +317,17 @@ h6 { display: inline; } +.follow-spinner { + display: none; +} +.htmx-request .follow-spinner { + display: inline; +} +.htmx-request.follow-spinner { + display: inline; +} + + /// Hide elements that require JS when scripting is disabled /// NOTE: This only seems to work when JS is actually disabled browser-wide, not via an extension like uBO /// See the callout on https://developer.mozilla.org/en-US/docs/Web/CSS/@media/scripting diff --git a/assets/views/user.jet.html b/assets/views/user.jet.html index d19863a..ae33313 100644 --- a/assets/views/user.jet.html +++ b/assets/views/user.jet.html @@ -27,7 +27,41 @@
-

{{ .User.Name }}

+ + + + +
+

{{ .User.Name }}

+ {{- if .User.IsFollowed == true && LoggedIn }} + + {{- else }} + + {{- end }} +
+ + + + + +

{{ .User.Following }} Following | {{ .User.MyPixiv }} MyPixiv

{{- if .User.Webpage }} diff --git a/core/requests.go b/core/requests.go index da1e524..8ea6f67 100644 --- a/core/requests.go +++ b/core/requests.go @@ -35,6 +35,45 @@ func init() { retryClient.Logger = nil // Disables the default logger in go-retryablehttp } +// Helper function to handle common request logic +func makeRequest(ctx context.Context, reqFunc func(context.Context, string) (*retryablehttp.Request, error), token *token_manager.Token, url string) (*SimpleHTTPResponse, error) { + req, err := reqFunc(ctx, token.Value) + if err != nil { + return nil, err + } + + start := time.Now() + resp, err := retryClient.Do(req) + end := time.Now() + + if err != nil { + return nil, i18n.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + audit.LogAPIRoundTrip(audit.APIRequestSpan{ + StartTime: start, + EndTime: end, + RequestId: request_context.GetFromContext(ctx).RequestId, + Response: resp, + Error: err, + Method: req.Method, + Url: url, + Token: token.Value, + Body: string(body), + }) + + return &SimpleHTTPResponse{ + StatusCode: resp.StatusCode, + Body: string(body), + }, nil +} + // retryRequest performs a request with automatic retries and token management func retryRequest( ctx context.Context, @@ -46,15 +85,22 @@ func retryRequest( var lastErr error tokenManager := config.GlobalConfig.TokenManager + if isPost { + // For POST requests, perform the request once without retrying + token := &token_manager.Token{Value: userToken} + return makeRequest(ctx, reqFunc, token, url) + } + + // For GET requests, use the retry logic for i := 0; i < config.GlobalConfig.APIMaxRetries; i++ { var token *token_manager.Token if userToken != "" { token = &token_manager.Token{Value: userToken} - } else if !isPost { + } else { token = tokenManager.GetToken() } - if token == nil && !isPost { + if token == nil { tokenManager.ResetAllTokens() return nil, i18n.Errorf( `All tokens (%d) are timed out, resetting all tokens to their initial good state. @@ -65,62 +111,20 @@ Please refer the following documentation for additional information: len(config.GlobalConfig.Token)) } - tokenValue := "" - if token != nil { - tokenValue = token.Value - } - - req, err := reqFunc(ctx, tokenValue) + resp, err := makeRequest(ctx, reqFunc, token, url) if err != nil { - return nil, err - } - - start := time.Now() - resp, err := retryClient.Do(req) - end := time.Now() - // Unwrap the body here so that we could log stuff correctly - if err != nil { - return nil, i18n.Errorf("failed to make request: %w", err) - } - // Validate response isn't nil - if resp == nil { - lastErr = i18n.Errorf("Received nil response after request: %w", err) + lastErr = err + tokenManager.MarkTokenStatus(token, token_manager.TimedOut) continue } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil || resp.Body == nil { - return nil, err - } - - audit.LogAPIRoundTrip(audit.APIRequestSpan{ - StartTime: start, - EndTime: end, - RequestId: request_context.GetFromContext(ctx).RequestId, - Response: resp, - Error: err, - Method: req.Method, - Url: url, - Token: tokenValue, - Body: string(body), - }) if resp.StatusCode == http.StatusOK { - if userToken == "" && !isPost { - tokenManager.MarkTokenStatus(token, token_manager.Good) - } - return &SimpleHTTPResponse{ - StatusCode: resp.StatusCode, - Body: string(body), - }, nil + tokenManager.MarkTokenStatus(token, token_manager.Good) + return resp, nil } lastErr = i18n.Errorf("HTTP status code: %d", resp.StatusCode) - - if userToken == "" && !isPost { - tokenManager.MarkTokenStatus(token, token_manager.TimedOut) - } + tokenManager.MarkTokenStatus(token, token_manager.TimedOut) select { case <-ctx.Done(): @@ -130,7 +134,7 @@ Please refer the following documentation for additional information: } } - return nil, i18n.Errorf("Max retries reached. Last error: %v", lastErr) + return nil, i18n.Errorf("Max retries reached for GET request. Last error: %v", lastErr) } // API_GET performs a GET request to the Pixiv API with automatic retries diff --git a/core/user.go b/core/user.go index 9c3a45c..7614e65 100644 --- a/core/user.go +++ b/core/user.go @@ -70,6 +70,8 @@ type User struct { NovelSeries []NovelSeries MangaSeries []MangaSeries + IsFollowed bool `json:"isFollowed"` // Denotes whether the logged in user currently following the given user + // The following fields are internal to PixivFE, used to display the number of works for a given category CountInfo CountInfo } diff --git a/i18n/locale/en/code.json b/i18n/locale/en/code.json index b1927a2..c688373 100644 --- a/i18n/locale/en/code.json +++ b/i18n/locale/en/code.json @@ -19,15 +19,14 @@ "core/ranking.go:Xy1LpykXW4E": "Retrying in %v", "core/ranking.go:_YcW1wsLOAg": "JSON unmarshalling successful on attempt %d", "core/ranking.go:sk-7WI2RS-Q": "Attempt %d of %d", - "core/requests.go:0Q07bJLKfvU": "Received nil response after request: %w", "core/requests.go:0hOvqlK-HwY": "HTTP status code: %d", "core/requests.go:7heLIvyBB2M": "Invalid JSON: %v", + "core/requests.go:7pvvVOIzWew": "Max retries reached for GET request. Last error: %v", "core/requests.go:CF6mVT22R2w": "Incompatible request body", "core/requests.go:Odhyy2SoIGU": "failed to make request: %w", "core/requests.go:XGjPq4l3s2o": "userToken is required for POST requests", "core/requests.go:XdMN7Q7DY3k": "failed to proxy request: %w", "core/requests.go:lLy9SHFUtQQ": "failed to copy response body: %w", - "core/requests.go:o7gE7trSD7c": "Max retries reached. Last error: %v", "core/requests.go:y3v5nQwdU0s": "All tokens (%d) are timed out, resetting all tokens to their initial good state.\nConsider providing additional tokens in PIXIVFE_TOKEN or reviewing API request level backoff configuration.\nPlease refer the following documentation for additional information:\n- https://pixivfe-docs.pages.dev/hosting/obtaining-pixivfe-token/\n- https://pixivfe-docs.pages.dev/hosting/environment-variables/#exponential-backoff-configuration", "core/user.go:1itpJBE-3VI": "Invalid page number.", "core/user.go:T3RwaHcnsYQ": "Invalid work category: %#v. Only \"%s\", \"%s\", \"%s\", \"%s\", \"%s\" and \"%s\" are available", diff --git a/i18n/locale/en/template.json b/i18n/locale/en/template.json index 446ab40..5662025 100644 --- a/i18n/locale/en/template.json +++ b/i18n/locale/en/template.json @@ -329,10 +329,13 @@ "assets/views/tag.jet.html:yHft19aQloM": "Minimum image width", "assets/views/unauthorized.jet.html:Q84lx6mjuV0": "Unauthorized", "assets/views/unauthorized.jet.html:SNRVjaimXDQ": "You need to log in to use this feature.", + "assets/views/user.jet.html:3MldbPX4xm8": "Loading...", "assets/views/user.jet.html:3_oq3Nw2b2U": "View on pixiv.net", "assets/views/user.jet.html:5xhgcv5yNyM": "Frequently used tags", + "assets/views/user.jet.html:7i2xxiolkqI": "Follow", "assets/views/user.jet.html:8ga3WlgRnzQ": "Illustrations and manga", "assets/views/user.jet.html:9EbrMpLP6_U": "Bookmarks", + "assets/views/user.jet.html:KSE10VbnTzI": "Following", "assets/views/user.jet.html:P6fO_UTqr7E": "Manga", "assets/views/user.jet.html:b84CRs16kKI": "View all", "assets/views/user.jet.html:clIffJhX-50": "{{ .User.Following }} Following | {{ .User.MyPixiv }} MyPixiv", diff --git a/server/audit/audit_init.go b/server/audit/audit_init.go index 38049ef..4273e55 100644 --- a/server/audit/audit_init.go +++ b/server/audit/audit_init.go @@ -12,7 +12,7 @@ import ( var ( optionSaveResponse bool MaxRecordedCount = 0 - logger *zap.Logger + Logger *zap.Logger ) // Init initializes the audit package and sets up response saving if enabled. @@ -70,7 +70,7 @@ func Init(saveResponse bool) error { // Build and assign the logger var err error - logger, err = zapConfig.Build() + Logger, err = zapConfig.Build() if err != nil { return i18n.Errorf("failed to initialize zap logger: %w", err) } @@ -82,7 +82,7 @@ func Init(saveResponse bool) error { // Handle saving responses MaxRecordedCount = 128 if err := os.MkdirAll(savePath, 0o700); err != nil { - logger.Error("Failed to create response save directory", + Logger.Error("Failed to create response save directory", zap.Error(err), zap.String("path", savePath), ) @@ -91,3 +91,8 @@ func Init(saveResponse bool) error { return nil } + +// GetLogger returns the initialized zap logger +func GetLogger() *zap.Logger { + return Logger +} diff --git a/server/audit/audit_log.go b/server/audit/audit_log.go index 4a7b46c..b0912d5 100644 --- a/server/audit/audit_log.go +++ b/server/audit/audit_log.go @@ -20,7 +20,7 @@ var RecordedRequestSpans = []Span{} func LogAndRecord(requestSpan Span) { duration := float64(Duration(requestSpan)) / float64(time.Second) - logger.Info("Request", + Logger.Info("Request", // Zap already prefixes the log with a timestamp // zap.String("timestamp", span.GetStartTime().Format(time.RFC3339)), zap.String("component", requestSpan.Component()), @@ -44,7 +44,7 @@ func LogAndRecord(requestSpan Span) { // It also logs any internal server errors that occurred during the request. func LogServerRoundTrip(requestSpan ServerRequestSpan) { if requestSpan.Error != nil { - logger.Error("Internal Server Error", + Logger.Error("Internal Server Error", zap.Error(requestSpan.Error), zap.String("requestId", requestSpan.RequestId), ) @@ -62,7 +62,7 @@ func LogAPIRoundTrip(requestSpan APIRequestSpan) { var err error requestSpan.ResponseFilename, err = writeResponseBodyToFile(requestSpan.Body) if err != nil { - logger.Error("Failed to save response to file", + Logger.Error("Failed to save response to file", zap.Error(err), zap.String("requestId", requestSpan.RequestId), ) @@ -70,7 +70,7 @@ func LogAPIRoundTrip(requestSpan APIRequestSpan) { } // Log a warning for non-2xx status codes if !(300 > requestSpan.Response.StatusCode && requestSpan.Response.StatusCode >= 200) { - logger.Warn("Non-2xx response from pixiv", + Logger.Warn("Non-2xx response from pixiv", zap.Int("status", requestSpan.Response.StatusCode), zap.String("requestId", requestSpan.RequestId), ) diff --git a/server/routes/actions.go b/server/routes/actions.go index 49695ca..7657ab7 100644 --- a/server/routes/actions.go +++ b/server/routes/actions.go @@ -9,6 +9,8 @@ import ( "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/session" "codeberg.org/vnpower/pixivfe/v2/server/utils" + "codeberg.org/vnpower/pixivfe/v2/server/audit" + "go.uber.org/zap" ) // NOTE: is the csrf protection by the upstream Pixiv API itself good enough, or do we need to implement our own? @@ -101,7 +103,12 @@ func LikeRoute(w http.ResponseWriter, r *http.Request) error { } func FollowUserRoute(w http.ResponseWriter, r *http.Request) error { + logger := audit.GetLogger() + + logger.Debug("FollowUserRoute called") + if r.Method != http.MethodPost { + logger.Debug("Method not allowed", zap.String("method", r.Method)) return i18n.Error("Method not allowed") } @@ -109,19 +116,23 @@ func FollowUserRoute(w http.ResponseWriter, r *http.Request) error { csrf := session.GetCookie(r, session.Cookie_CSRF) if token == "" || csrf == "" { + logger.Debug("User not logged in or missing CSRF") return PromptUserToLoginPage(w, r) } id := GetPathVar(r, "id") if id == "" { + logger.Debug("No user ID provided") return i18n.Error("No user ID provided.") } + logger.Debug("Following user", zap.String("user_id", id)) isPrivate := r.FormValue("private") == "true" restrict := "0" if isPrivate { restrict = "1" } + logger.Debug("Follow privacy setting", zap.Bool("isPrivate", isPrivate)) URL := "https://www.pixiv.net/bookmark_add.php" payload := url.Values{ @@ -133,16 +144,25 @@ func FollowUserRoute(w http.ResponseWriter, r *http.Request) error { "format": {"json"}, }.Encode() + logger.Debug("Making API call to follow user", zap.String("URL", URL)) if err := core.API_POST(r.Context(), URL, payload, token, csrf, false); err != nil { + logger.Debug("API call failed", zap.Error(err)) return err } + logger.Debug("API call successful") + logger.Debug("Redirecting user") utils.RedirectToWhenceYouCame(w, r) return nil } func UnfollowUserRoute(w http.ResponseWriter, r *http.Request) error { + logger := audit.GetLogger() + + logger.Debug("UnfollowUserRoute called") + if r.Method != http.MethodPost { + logger.Debug("Method not allowed", zap.String("method", r.Method)) return i18n.Error("Method not allowed") } @@ -150,13 +170,16 @@ func UnfollowUserRoute(w http.ResponseWriter, r *http.Request) error { csrf := session.GetCookie(r, session.Cookie_CSRF) if token == "" || csrf == "" { + logger.Debug("User not logged in or missing CSRF") return PromptUserToLoginPage(w, r) } id := GetPathVar(r, "id") if id == "" { + logger.Debug("No user ID provided") return i18n.Error("No user ID provided.") } + logger.Debug("Unfollowing user", zap.String("user_id", id)) URL := "https://www.pixiv.net/rpc_group_setting.php" payload := url.Values{ @@ -165,10 +188,14 @@ func UnfollowUserRoute(w http.ResponseWriter, r *http.Request) error { "id": {id}, }.Encode() + logger.Debug("Making API call to unfollow user", zap.String("URL", URL)) if err := core.API_POST(r.Context(), URL, payload, token, csrf, false); err != nil { + logger.Debug("API call failed", zap.Error(err)) return err } + logger.Debug("API call successful") + logger.Debug("Redirecting user") utils.RedirectToWhenceYouCame(w, r) return nil }