mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
user follow: use mobile api
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user