user follow: use mobile api

This commit is contained in:
perennial
2024-10-13 19:55:59 +11:00
parent db529d56c1
commit aad63d216a
4 changed files with 107 additions and 45 deletions
+1 -8
View File
@@ -28,8 +28,6 @@
<!-- User name and social media links -->
<div class="col-12 col-md-9">
<!-- Follow and unfollow buttons -->
<div class="d-flex justify-content-start justify-content-xl-between align-items-center">
<h1>{{ .User.Name }}</h1>
@@ -47,7 +45,7 @@
{{- 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><i class="bi bi-person 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>
@@ -57,11 +55,6 @@
{{- end }}
</div>
<p class="text-muted">{{ .User.Following }} Following | {{ .User.MyPixiv }} MyPixiv</p>
<div class="mb-3">
{{- if .User.Webpage }}
+48 -8
View File
@@ -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 {
+1
View File
@@ -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.",
+57 -29
View File
@@ -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)