Loading...
@@ -57,11 +55,6 @@
{{- end }}
-
-
-
-
-
{{ .User.Following }} Following | {{ .User.MyPixiv }} MyPixiv
{{- if .User.Webpage }}
diff --git a/core/requests.go b/core/requests.go
index 41656d7..0899137 100644
--- a/core/requests.go
+++ b/core/requests.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
+ "mime/multipart"
"net/http"
"time"
@@ -180,17 +181,59 @@ func API_GET_UnwrapJson(ctx context.Context, url string, userToken string) (stri
return gjson.Get(resp.Body, "body").String(), nil
}
-// API_POST performs a POST request to the Pixiv API with automatic retries
-func API_POST(ctx context.Context, url, payload, userToken, csrf string, isJSON bool) (*SimpleHTTPResponse, error) {
+// createMultipartFormData is a helper function to create multipart form data
+func createMultipartFormData(fields map[string]string) (*bytes.Buffer, string, error) {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ for key, value := range fields {
+ err := writer.WriteField(key, value)
+ if err != nil {
+ return nil, "", err
+ }
+ }
+ err := writer.Close()
+ if err != nil {
+ return nil, "", err
+ }
+ return body, writer.FormDataContentType(), nil
+}
+
+// API_POST performs a POST request to the Pixiv API
+func API_POST(
+ ctx context.Context,
+ url string,
+ payload interface{},
+ userToken, csrf string,
+ contentType string,
+) (*SimpleHTTPResponse, error) {
if userToken == "" {
return nil, i18n.Error("userToken is required for POST requests")
}
resp, err := retryRequest(ctx, func(ctx context.Context, token string) (*retryablehttp.Request, error) {
- req, err := retryablehttp.NewRequest("POST", url, bytes.NewBuffer([]byte(payload)))
+ var req *retryablehttp.Request
+ var err error
+
+ switch v := payload.(type) {
+ case string:
+ req, err = retryablehttp.NewRequest("POST", url, bytes.NewBuffer([]byte(v)))
+ case map[string]string:
+ body, formContentType, err := createMultipartFormData(v)
+ if err != nil {
+ return nil, err
+ }
+ req, err = retryablehttp.NewRequest("POST", url, body)
+ if err == nil {
+ contentType = formContentType
+ }
+ default:
+ return nil, i18n.Error("Unsupported payload type")
+ }
+
if err != nil {
return nil, err
}
+
req = req.WithContext(ctx)
req.Header.Add("User-Agent", config.GetRandomUserAgent())
req.Header.Add("Accept", "application/json")
@@ -199,11 +242,8 @@ func API_POST(ctx context.Context, url, payload, userToken, csrf string, isJSON
Name: "PHPSESSID",
Value: token,
})
- if isJSON {
- req.Header.Add("Content-Type", "application/json; charset=utf-8")
- } else {
- req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
- }
+ req.Header.Add("Content-Type", contentType)
+
return req, nil
}, userToken, true, url)
if err != nil {
diff --git a/i18n/locale/en/code.json b/i18n/locale/en/code.json
index c688373..b1d7beb 100644
--- a/i18n/locale/en/code.json
+++ b/i18n/locale/en/code.json
@@ -26,6 +26,7 @@
"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:f78uqyomFJk": "Unsupported payload type",
"core/requests.go:lLy9SHFUtQQ": "failed to copy response body: %w",
"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.",
diff --git a/server/routes/actions.go b/server/routes/actions.go
index ee92ef0..d1a9d4c 100644
--- a/server/routes/actions.go
+++ b/server/routes/actions.go
@@ -1,9 +1,10 @@
package routes
import (
+ "bytes"
"fmt"
+ "mime/multipart"
"net/http"
- "net/url"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
@@ -41,12 +42,14 @@ func AddBookmarkRoute(w http.ResponseWriter, r *http.Request) error {
URL := "https://www.pixiv.net/ajax/illusts/bookmarks/add"
payload := fmt.Sprintf(`{
-"illust_id": "%s",
-"restrict": 0,
-"comment": "",
-"tags": []
-}`, id)
- _, err := core.API_POST(r.Context(), URL, payload, token, csrf, true)
+ "illust_id": "%s",
+ "restrict": 0,
+ "comment": "",
+ "tags": []
+ }`, id)
+
+ contentType := "application/json; charset=utf-8"
+ _, err := core.API_POST(r.Context(), URL, payload, token, csrf, contentType)
if err != nil {
logger.Error("API call failed", zap.Error(err))
return err
@@ -75,10 +78,11 @@ func DeleteBookmarkRoute(w http.ResponseWriter, r *http.Request) error {
return i18n.Error("No ID provided.")
}
- // You can't unlike
URL := "https://www.pixiv.net/ajax/illusts/bookmarks/delete"
- payload := fmt.Sprintf(`bookmark_id=%s`, id)
- _, err := core.API_POST(r.Context(), URL, payload, token, csrf, false)
+ payload := fmt.Sprintf("bookmark_id=%s", id)
+
+ contentType := "application/x-www-form-urlencoded; charset=utf-8"
+ _, err := core.API_POST(r.Context(), URL, payload, token, csrf, contentType)
if err != nil {
logger.Error("API call failed", zap.Error(err))
return err
@@ -109,7 +113,9 @@ func LikeRoute(w http.ResponseWriter, r *http.Request) error {
URL := "https://www.pixiv.net/ajax/illusts/like"
payload := fmt.Sprintf(`{"illust_id": "%s"}`, id)
- _, err := core.API_POST(r.Context(), URL, payload, token, csrf, true)
+
+ contentType := "application/json; charset=utf-8"
+ _, err := core.API_POST(r.Context(), URL, payload, token, csrf, contentType)
if err != nil {
logger.Error("API call failed", zap.Error(err))
return err
@@ -119,6 +125,19 @@ func LikeRoute(w http.ResponseWriter, r *http.Request) error {
return nil
}
+/*
+NOTE: we're using the mobile API for FollowUserRoute and UnfollowUserRoute since it's an actual AJAX API
+ instead of some weird php thing for the usual desktop routes (/bookmark_add.php and /rpc_group_setting.php)
+
+ the desktop routes return HTML for the pixiv SPA when they feel like it and don't return helpful responses
+ when you send a request that doesn't perfectly meet their specifications, making troubleshooting a nightmare
+
+ for comparison, the mobile API worked first try without any issues
+
+ interestingly enough, replicating the requests for the desktop routes via cURL worked fine but a Go implementation
+ just refused to work
+*/
+
func FollowUserRoute(w http.ResponseWriter, r *http.Request) error {
logger := getLogger()
@@ -151,18 +170,22 @@ func FollowUserRoute(w http.ResponseWriter, r *http.Request) error {
}
logger.Debug("Follow privacy setting", zap.Bool("isPrivate", isPrivate))
- URL := "https://www.pixiv.net/bookmark_add.php"
- payload := url.Values{
- "mode": {"add"},
- "type": {"user"},
- "user_id": {id},
- "tag": {""},
- "restrict": {restrict},
- "format": {"json"},
- }.Encode()
+ URL := "https://www.pixiv.net/touch/ajax_api/ajax_api.php"
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ writer.WriteField("mode", "add_bookmark_user")
+ writer.WriteField("restrict", restrict)
+ writer.WriteField("user_id", id)
+ writer.Close()
logger.Debug("Making API call to follow user", zap.String("URL", URL))
- resp, err := core.API_POST(r.Context(), URL, payload, token, csrf, false)
+ fields := map[string]string{
+ "mode": "add_bookmark_user",
+ "restrict": restrict,
+ "user_id": id,
+ }
+ resp, err := core.API_POST(r.Context(), URL, fields, token, csrf, "")
if err != nil {
logger.Error("API call failed", zap.Error(err))
return err
@@ -199,21 +222,26 @@ func UnfollowUserRoute(w http.ResponseWriter, r *http.Request) error {
}
logger.Debug("Unfollowing user", zap.String("user_id", id))
- URL := "https://www.pixiv.net/rpc_group_setting.php"
- payload := url.Values{
- "mode": {"del"},
- "type": {"bookuser"},
- "id": {id},
- }.Encode()
+ URL := "https://www.pixiv.net/touch/ajax_api/ajax_api.php"
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ writer.WriteField("mode", "delete_bookmark_user")
+ writer.WriteField("user_id", id)
+ writer.Close()
logger.Debug("Making API call to unfollow user", zap.String("URL", URL))
- _, err := core.API_POST(r.Context(), URL, payload, token, csrf, false)
+ fields := map[string]string{
+ "mode": "delete_bookmark_user",
+ "user_id": id,
+ }
+ resp, err := core.API_POST(r.Context(), URL, fields, token, csrf, "")
if err != nil {
logger.Error("API call failed", zap.Error(err))
return err
}
- logger.Debug("API call successful")
+ logger.Debug("API call successful", zap.Int("StatusCode", resp.StatusCode), zap.String("Body", resp.Body))
logger.Debug("Redirecting user")
utils.RedirectToWhenceYouCame(w, r)