mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
debug wip
This commit is contained in:
Vendored
+12
@@ -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;
|
||||
|
||||
Vendored
+11
@@ -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
|
||||
|
||||
@@ -27,7 +27,41 @@
|
||||
</div>
|
||||
<!-- User name and social media links -->
|
||||
<div class="col-12 col-md-9">
|
||||
<h1>{{ .User.Name }}</h1>
|
||||
|
||||
|
||||
|
||||
<!-- Follow and unfollow buttons -->
|
||||
<div class="d-flex justify-content-start justify-content-xl-between align-items-center">
|
||||
<h1>{{ .User.Name }}</h1>
|
||||
{{- if .User.IsFollowed == true && LoggedIn }}
|
||||
<button type="button" class="custom-btn-secondary text-nowrap me-2" hx-post="/self/unfollowUser/{{ .User.ID }}" hx-target="body" hx-indicator="#follow-spinner" hx-push-url="true">
|
||||
<div class="d-flex">
|
||||
<div><i class="bi bi-person-plus-fill me-2"></i>Following</div>
|
||||
<div class="follow-spinner ms-2" id="follow-spinner">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{{- else }}
|
||||
<button type="button" class="custom-btn-secondary text-nowrap me-2" hx-post="/self/followUser/{{ .User.ID }}" hx-target="body" hx-indicator="#follow-spinner" hx-push-url="true">
|
||||
<div class="d-flex">
|
||||
<div><i class="bi bi-person-plus me-2"></i>Follow</div>
|
||||
<div class="follow-spinner ms-2" id="follow-spinner">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{{- end }}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="text-muted">{{ .User.Following }} Following | {{ .User.MyPixiv }} MyPixiv</p>
|
||||
<div class="mb-3">
|
||||
{{- if .User.Webpage }}
|
||||
|
||||
+55
-51
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <a href=\"/settings#login\">log in</a> 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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user