server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"everypublish/server/internal/credentials"
|
||||
"everypublish/server/internal/platform"
|
||||
)
|
||||
|
||||
// Adapter implements the server-side Bilibili API boundary. The exported
|
||||
// endpoint fields make the adapter straightforward to use with an httptest
|
||||
// server while retaining production defaults in New.
|
||||
type Adapter struct {
|
||||
Store *credentials.Store
|
||||
Credentials *credentials.Store // compatibility alias for callers using this name
|
||||
HTTP *http.Client
|
||||
MemberBase string
|
||||
PassportBase string
|
||||
UpOSScheme string
|
||||
PollTimeout time.Duration
|
||||
PollInterval time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
func New(store *credentials.Store) *Adapter {
|
||||
return &Adapter{Store: store, Credentials: store, HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||
MemberBase: "https://member.bilibili.com", PassportBase: "https://passport.bilibili.com",
|
||||
UpOSScheme: "https", PollTimeout: 3 * time.Minute, PollInterval: 2 * time.Second, MaxRetries: 2}
|
||||
}
|
||||
|
||||
func (a *Adapter) Platform() string { return "bilibili" }
|
||||
|
||||
func (a *Adapter) BeginLogin(ctx context.Context) (platform.LoginSession, error) {
|
||||
var ret struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
Key string `json:"qrcode_key"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := a.getJSON(ctx, a.passportBase()+"/x/passport-login/web/qrcode/generate", "", &ret); err != nil {
|
||||
return platform.LoginSession{}, err
|
||||
}
|
||||
if ret.Code != 0 || ret.Data.Key == "" || ret.Data.URL == "" {
|
||||
return platform.LoginSession{}, perr(platform.PlatformChanged, "invalid QR response", nil)
|
||||
}
|
||||
return platform.LoginSession{Token: ret.Data.Key, URL: ret.Data.URL}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) PollLogin(ctx context.Context, token string) (platform.LoginPoll, error) {
|
||||
if token == "" {
|
||||
return platform.LoginPoll{}, perr(platform.Validation, "QR token is empty", nil)
|
||||
}
|
||||
q := url.Values{"qrcode_key": []string{token}}
|
||||
var ret struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := a.getJSON(ctx, a.passportBase()+"/x/passport-login/web/qrcode/poll?"+q.Encode(), "", &ret); err != nil {
|
||||
return platform.LoginPoll{}, err
|
||||
}
|
||||
switch ret.Code {
|
||||
case 86101:
|
||||
return platform.LoginPoll{State: platform.LoginPending, Prompt: "请使用哔哩哔哩手机客户端扫码"}, nil
|
||||
case 86090:
|
||||
return platform.LoginPoll{State: platform.LoginScanned, Prompt: "已扫码,请在哔哩哔哩手机客户端确认登录"}, nil
|
||||
case 0:
|
||||
cookies := cookiesFromURL(ret.Data.URL)
|
||||
if cookies == "" {
|
||||
return platform.LoginPoll{}, perr(platform.PlatformChanged, "login response did not contain cookies", nil)
|
||||
}
|
||||
return platform.LoginPoll{State: platform.LoginConfirmed, Cookies: cookies}, nil
|
||||
default:
|
||||
return platform.LoginPoll{}, perr(platform.PlatformChanged, "QR poll rejected: "+ret.Message, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) SaveCredentials(workspaceID, accountID uint64, cookies string) error {
|
||||
if strings.TrimSpace(cookies) == "" {
|
||||
return perr(platform.Validation, "cookies are empty", nil)
|
||||
}
|
||||
if cookieValue(cookies, "SESSDATA") == "" || cookieValue(cookies, "bili_jct") == "" || cookieValue(cookies, "DedeUserID") == "" {
|
||||
return perr(platform.Validation, "Bilibili cookies must include SESSDATA, bili_jct and DedeUserID", nil)
|
||||
}
|
||||
s := a.store()
|
||||
if s == nil {
|
||||
return perr(platform.Unknown, "credential store is not configured", nil)
|
||||
}
|
||||
if err := s.Save(workspaceID, accountID, []byte(cookies)); err != nil {
|
||||
return perr(platform.Unknown, "saving credentials", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) CheckCredentials(ctx context.Context, workspaceID, accountID uint64) error {
|
||||
s := a.store()
|
||||
if s == nil {
|
||||
return perr(platform.Unknown, "credential store is not configured", nil)
|
||||
}
|
||||
raw, err := s.Load(workspaceID, accountID)
|
||||
if err != nil {
|
||||
return perr(platform.AuthRequired, "credentials are unavailable", err)
|
||||
}
|
||||
var ret struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
IsLogin bool `json:"isLogin"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := a.getJSON(ctx, a.memberBase()+"/x/web-interface/nav", string(raw), &ret); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Code != 0 || !ret.Data.IsLogin {
|
||||
return perr(platform.AuthRequired, "Bilibili login has expired", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Publish(ctx context.Context, in platform.PublishInput) (platform.PublishResult, error) {
|
||||
s := a.store()
|
||||
if s == nil {
|
||||
return platform.PublishResult{}, perr(platform.Unknown, "credential store is not configured", nil)
|
||||
}
|
||||
raw, err := s.Load(in.WorkspaceID, in.AccountID)
|
||||
if err != nil {
|
||||
return platform.PublishResult{}, perr(platform.AuthRequired, "credentials are unavailable", err)
|
||||
}
|
||||
if len(in.MaterialURLs) == 0 {
|
||||
return platform.PublishResult{}, perr(platform.Validation, "material is required", nil)
|
||||
}
|
||||
if len(in.MaterialURLs) > 1 {
|
||||
return platform.PublishResult{}, perr(platform.Validation, "Bilibili currently accepts one video per task", nil)
|
||||
}
|
||||
path, cleanup, err := a.download(ctx, in.MaterialURLs[0])
|
||||
if err != nil {
|
||||
return platform.PublishResult{}, err
|
||||
}
|
||||
defer cleanup()
|
||||
part, err := a.upload(ctx, string(raw), path)
|
||||
if err != nil {
|
||||
return platform.PublishResult{}, err
|
||||
}
|
||||
bvid, err := a.submit(ctx, string(raw), in, part)
|
||||
if err != nil {
|
||||
return platform.PublishResult{}, err
|
||||
}
|
||||
if bvid == "" {
|
||||
return platform.PublishResult{}, perr(platform.PlatformChanged, "submission did not return bvid", nil)
|
||||
}
|
||||
return platform.PublishResult{URL: "https://www.bilibili.com/video/" + bvid, Receipt: bvid}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) download(ctx context.Context, rawURL string) (string, func(), error) {
|
||||
raw, err := a.request(ctx, http.MethodGet, rawURL, "", nil, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
f, err := os.CreateTemp("", "ep-bilibili-*")
|
||||
if err != nil {
|
||||
return "", nil, perr(platform.Unknown, "creating temporary material", err)
|
||||
}
|
||||
name := f.Name()
|
||||
if _, err = f.Write(raw); err != nil {
|
||||
f.Close()
|
||||
os.Remove(name)
|
||||
return "", nil, perr(platform.Network, "saving material", err)
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
os.Remove(name)
|
||||
return "", nil, perr(platform.Network, "saving material", err)
|
||||
}
|
||||
return name, func() { _ = os.Remove(name) }, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) upload(ctx context.Context, cookies, path string) (map[string]interface{}, error) {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, perr(platform.Validation, "material unavailable", err)
|
||||
}
|
||||
name, total := filepath.Base(path), st.Size()
|
||||
if total <= 0 {
|
||||
return nil, perr(platform.Validation, "material is empty", nil)
|
||||
}
|
||||
q := url.Values{"r": {"upos"}, "profile": {"ugcupos/bup"}, "ssl": {"0"}, "version": {"2.8.12"}, "build": {"2081200"}, "name": {name}, "size": {strconv.FormatInt(total, 10)}}
|
||||
var pre struct {
|
||||
OK int `json:"OK"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Auth string `json:"auth"`
|
||||
BizID int `json:"biz_id"`
|
||||
UposURI string `json:"upos_uri"`
|
||||
ChunkSize int `json:"chunk_size"`
|
||||
}
|
||||
if err := a.getJSON(ctx, a.memberBase()+"/preupload?upcdn=bda2&probe_version=20221109&"+q.Encode(), cookies, &pre); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pre.OK != 1 || pre.Endpoint == "" || pre.UposURI == "" {
|
||||
return nil, perr(platform.PlatformChanged, "invalid preupload response", nil)
|
||||
}
|
||||
upURL := a.upURL(pre.Endpoint, pre.UposURI)
|
||||
var up struct {
|
||||
UploadID string `json:"upload_id"`
|
||||
}
|
||||
if err := a.postJSON(ctx, upURL+"?uploads&output=json", cookies, map[string]string{"X-Upos-Auth": pre.Auth}, nil, &up); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if up.UploadID == "" {
|
||||
return nil, perr(platform.PlatformChanged, "upload id missing", nil)
|
||||
}
|
||||
chunk := pre.ChunkSize
|
||||
if chunk <= 0 {
|
||||
chunk = 7 * 1024 * 1024
|
||||
}
|
||||
count := int((total + int64(chunk) - 1) / int64(chunk))
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, perr(platform.Validation, "material unavailable", err)
|
||||
}
|
||||
defer f.Close()
|
||||
parts := make([]map[string]interface{}, 0, count)
|
||||
buf := make([]byte, chunk)
|
||||
for i := 0; i < count; i++ {
|
||||
n, er := io.ReadFull(f, buf)
|
||||
if er != nil && er != io.ErrUnexpectedEOF && er != io.EOF {
|
||||
return nil, perr(platform.Network, "reading material", er)
|
||||
}
|
||||
pq := url.Values{"uploadId": {up.UploadID}, "partNumber": {strconv.Itoa(i + 1)}, "chunk": {strconv.Itoa(i)}, "chunks": {strconv.Itoa(count)}, "size": {strconv.Itoa(n)}, "start": {strconv.Itoa(i * chunk)}, "end": {strconv.Itoa(i*chunk + n)}, "total": {strconv.FormatInt(total, 10)}}
|
||||
etag, err := a.put(ctx, upURL+"?"+pq.Encode(), cookies, map[string]string{"X-Upos-Auth": pre.Auth}, buf[:n])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: part %d", err, i+1)
|
||||
}
|
||||
if etag == "" {
|
||||
return nil, perr(platform.PlatformChanged, "upload response missing ETag", nil)
|
||||
}
|
||||
parts = append(parts, map[string]interface{}{"partNumber": i + 1, "eTag": etag})
|
||||
}
|
||||
mq := url.Values{"name": {name}, "uploadId": {up.UploadID}, "biz_id": {strconv.Itoa(pre.BizID)}, "output": {"json"}, "profile": {"ugcupos/bup"}}
|
||||
var merged struct {
|
||||
OK int `json:"OK"`
|
||||
}
|
||||
if err := a.postJSON(ctx, upURL+"?"+mq.Encode(), cookies, map[string]string{"X-Upos-Auth": pre.Auth}, map[string]interface{}{"parts": parts}, &merged); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if merged.OK != 1 {
|
||||
return nil, perr(platform.PlatformChanged, "merge failed", nil)
|
||||
}
|
||||
base := strings.TrimSuffix(filepath.Base(pre.UposURI), filepath.Ext(filepath.Base(pre.UposURI)))
|
||||
return map[string]interface{}{"title": strings.TrimSuffix(name, filepath.Ext(name)), "filename": base, "desc": ""}, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) submit(ctx context.Context, cookies string, in platform.PublishInput, part map[string]interface{}) (string, error) {
|
||||
tag := strings.Join(in.Tags, ",")
|
||||
if tag == "" {
|
||||
tag = "日常"
|
||||
}
|
||||
tid := in.CategoryID
|
||||
if tid <= 0 {
|
||||
tid = 174
|
||||
}
|
||||
body := map[string]interface{}{"title": truncate(in.Title, 80), "desc": in.Content, "desc_v2": []map[string]interface{}{{"raw_text": in.Content, "type": 1, "biz_id": ""}}, "copyright": 1, "source": "", "tid": tid, "tag": tag, "dynamic": "", "videos": []map[string]interface{}{part}}
|
||||
q := url.Values{"csrf": {cookieValue(cookies, "bili_jct")}}
|
||||
var ret struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Bvid string `json:"bvid"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := a.postJSON(ctx, a.memberBase()+"/x/vu/web/add?"+q.Encode(), cookies, nil, body, &ret); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ret.Code != 0 {
|
||||
return "", perr(platform.Validation, ret.Message, nil)
|
||||
}
|
||||
return ret.Data.Bvid, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) getJSON(ctx context.Context, rawURL, cookies string, out interface{}) error {
|
||||
raw, err := a.request(ctx, http.MethodGet, rawURL, cookies, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return perr(platform.PlatformChanged, "invalid JSON response", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) postJSON(ctx context.Context, rawURL, cookies string, headers map[string]string, body, out interface{}) error {
|
||||
var data io.Reader
|
||||
if body != nil {
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return perr(platform.Validation, "invalid request", err)
|
||||
}
|
||||
data = bytes.NewReader(raw)
|
||||
}
|
||||
raw, err := a.request(ctx, http.MethodPost, rawURL, cookies, headers, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out != nil {
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return perr(platform.PlatformChanged, "invalid JSON response", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) put(ctx context.Context, rawURL, cookies string, headers map[string]string, body []byte) (string, error) {
|
||||
for attempt := 0; attempt <= a.retries(); attempt++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", perr(platform.Validation, "invalid URL", err)
|
||||
}
|
||||
if cookies != "" {
|
||||
req.Header.Set("Cookie", cookies)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
req.Header.Set("Referer", "https://member.bilibili.com")
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := a.client().Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", err
|
||||
}
|
||||
if attempt < a.retries() {
|
||||
if !sleep(ctx, retryDelay(attempt)) {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
return "", perr(platform.Network, "network request failed", err)
|
||||
}
|
||||
_, readErr := io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if readErr != nil {
|
||||
return "", perr(platform.Network, "reading response", readErr)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if resp.StatusCode >= 500 && attempt < a.retries() {
|
||||
if !sleep(ctx, retryDelay(attempt)) {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
cat := platform.Unknown
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
cat = platform.AuthRequired
|
||||
}
|
||||
if resp.StatusCode == 429 {
|
||||
cat = platform.RateLimited
|
||||
}
|
||||
if resp.StatusCode >= 500 {
|
||||
cat = platform.Network
|
||||
}
|
||||
return "", perr(cat, "HTTP "+strconv.Itoa(resp.StatusCode), nil)
|
||||
}
|
||||
return strings.Trim(resp.Header.Get("ETag"), "\""), nil
|
||||
}
|
||||
return "", perr(platform.Network, "upload failed", nil)
|
||||
}
|
||||
|
||||
func (a *Adapter) request(ctx context.Context, method, rawURL, cookies string, headers map[string]string, body io.Reader) ([]byte, error) {
|
||||
var payload []byte
|
||||
var err error
|
||||
if body != nil {
|
||||
payload, err = io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, perr(platform.Network, "reading request body", err)
|
||||
}
|
||||
}
|
||||
var last error
|
||||
for attempt := 0; attempt <= a.retries(); attempt++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
reqBody = bytes.NewReader(payload)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, rawURL, reqBody)
|
||||
if err != nil {
|
||||
return nil, perr(platform.Validation, "invalid URL", err)
|
||||
}
|
||||
if cookies != "" {
|
||||
req.Header.Set("Cookie", cookies)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
req.Header.Set("Referer", "https://member.bilibili.com")
|
||||
if body != nil && method == http.MethodPost {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := a.client().Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
last = perr(platform.Network, "network request failed", err)
|
||||
if attempt < a.retries() {
|
||||
if !sleep(ctx, retryDelay(attempt)) {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
raw, readErr := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
||||
resp.Body.Close()
|
||||
if readErr != nil {
|
||||
return nil, perr(platform.Network, "reading response", readErr)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
cat := platform.Unknown
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
cat = platform.AuthRequired
|
||||
}
|
||||
if resp.StatusCode == 429 {
|
||||
cat = platform.RateLimited
|
||||
}
|
||||
if resp.StatusCode >= 500 {
|
||||
cat = platform.Network
|
||||
}
|
||||
last = perr(cat, "HTTP "+strconv.Itoa(resp.StatusCode), nil)
|
||||
if resp.StatusCode >= 500 && attempt < a.retries() {
|
||||
if !sleep(ctx, retryDelay(attempt)) {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
return nil, last
|
||||
}
|
||||
|
||||
func (a *Adapter) client() *http.Client {
|
||||
if a.HTTP != nil {
|
||||
return a.HTTP
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
func (a *Adapter) retries() int {
|
||||
if a.MaxRetries < 0 {
|
||||
return 0
|
||||
}
|
||||
if a.MaxRetries == 0 {
|
||||
return 2
|
||||
}
|
||||
return a.MaxRetries
|
||||
}
|
||||
func retryDelay(n int) time.Duration { return time.Duration(25*(1<<n)) * time.Millisecond }
|
||||
func sleep(ctx context.Context, d time.Duration) bool {
|
||||
t := time.NewTimer(d)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-t.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
func (a *Adapter) memberBase() string {
|
||||
if a.MemberBase != "" {
|
||||
return strings.TrimRight(a.MemberBase, "/")
|
||||
}
|
||||
return "https://member.bilibili.com"
|
||||
}
|
||||
func (a *Adapter) passportBase() string {
|
||||
if a.PassportBase != "" {
|
||||
return strings.TrimRight(a.PassportBase, "/")
|
||||
}
|
||||
return "https://passport.bilibili.com"
|
||||
}
|
||||
func (a *Adapter) store() *credentials.Store {
|
||||
if a.Store != nil {
|
||||
return a.Store
|
||||
}
|
||||
return a.Credentials
|
||||
}
|
||||
func (a *Adapter) upURL(endpoint, uri string) string {
|
||||
scheme := a.UpOSScheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
|
||||
return strings.TrimRight(endpoint, "/") + "/" + strings.TrimPrefix(uri, "upos://")
|
||||
}
|
||||
return scheme + "://" + strings.TrimRight(strings.TrimPrefix(endpoint, "//"), "/") + "/" + strings.TrimPrefix(uri, "upos://")
|
||||
}
|
||||
|
||||
// cookiesFromURL extracts only the three cookies accepted by Bilibili's
|
||||
// cross-domain login redirect; all query values are URL-decoded by net/url.
|
||||
func cookiesFromURL(raw string) string {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
q := u.Query()
|
||||
sess := q.Get("SESSDATA")
|
||||
csrf := q.Get("bili_jct")
|
||||
uid := q.Get("DedeUserID")
|
||||
if sess == "" || csrf == "" || uid == "" {
|
||||
return ""
|
||||
}
|
||||
return "SESSDATA=" + sess + "; bili_jct=" + csrf + "; DedeUserID=" + uid
|
||||
}
|
||||
func cookieValue(cookies, key string) string {
|
||||
for _, p := range strings.Split(cookies, ";") {
|
||||
p = strings.TrimSpace(p)
|
||||
i := strings.IndexByte(p, '=')
|
||||
if i > 0 && p[:i] == key {
|
||||
return p[i+1:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func truncate(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) > n {
|
||||
return string(r[:n])
|
||||
}
|
||||
return s
|
||||
}
|
||||
func perr(cat platform.ErrorCategory, msg string, err error) error {
|
||||
return &platform.Error{Category: cat, Message: msg, Err: err}
|
||||
}
|
||||
Reference in New Issue
Block a user