feat: expose device pairing over HTTP
This commit is contained in:
1 parent
e95476185b
commit
1157843f50
23 files changed
+587
-75
No files matched your search
+39
-6
@@ -8,12 +8,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
patternAdminCreateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
patternAdminUpdateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{0,32}$`)
|
||||
patternLoginRequestDevice = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
patternLoginRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternRegisterRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternTrackIDWhenBilibili = regexp.MustCompile(`^BV[1-9A-HJ-NP-Za-km-z]{10}(?:/([2-9]|[1-9][0-9]{1,3}))?$`)
|
||||
patternAdminCreateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{1,32}$`)
|
||||
patternAdminUpdateRoomRequestSlug = regexp.MustCompile(`^[a-z0-9-]{0,32}$`)
|
||||
patternLoginRequestDevice = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
|
||||
patternLoginRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternRedeemPairingCodeRequestCode = regexp.MustCompile(`^[2-9A-HJ-NP-Z]{8}$`)
|
||||
patternRegisterRequestUsername = regexp.MustCompile(`^[a-z0-9_-]{3,32}$`)
|
||||
patternTrackIDWhenBilibili = regexp.MustCompile(`^BV[1-9A-HJ-NP-Za-km-z]{10}(?:/([2-9]|[1-9][0-9]{1,3}))?$`)
|
||||
)
|
||||
|
||||
func decodeConnectionRequest(typ string, raw json.RawMessage, path string) (any, bool, error) {
|
||||
@@ -203,6 +204,14 @@ func DecodeCopySharedPlaylistRequest(body []byte) (CopySharedPlaylistRequest, er
|
||||
return decodeCopySharedPlaylistRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeCreatePairingCodeRequest(body []byte) (CreatePairingCodeRequest, error) {
|
||||
return decodeCreatePairingCodeRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeRedeemPairingCodeRequest(body []byte) (RedeemPairingCodeRequest, error) {
|
||||
return decodeRedeemPairingCodeRequest(body, "")
|
||||
}
|
||||
|
||||
func decodeAddMyPlaylistItemRequest(raw json.RawMessage, path string) (AddMyPlaylistItemRequest, error) {
|
||||
var v AddMyPlaylistItemRequest
|
||||
m, err := decodeObject(raw, path, "track")
|
||||
@@ -554,6 +563,12 @@ func decodeCreateMyPlaylistRequest(raw json.RawMessage, path string) (CreateMyPl
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeCreatePairingCodeRequest(raw json.RawMessage, path string) (CreatePairingCodeRequest, error) {
|
||||
var v CreatePairingCodeRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeCreateRoomRequest(raw json.RawMessage, path string) (CreateRoomRequest, error) {
|
||||
var v CreateRoomRequest
|
||||
m, err := decodeObject(raw, path, "description", "name", "password")
|
||||
@@ -980,6 +995,24 @@ func decodeRecommendListRequest(raw json.RawMessage, path string) (RecommendList
|
||||
return v, err
|
||||
}
|
||||
|
||||
func decodeRedeemPairingCodeRequest(raw json.RawMessage, path string) (RedeemPairingCodeRequest, error) {
|
||||
var v RedeemPairingCodeRequest
|
||||
m, err := decodeObject(raw, path, "code")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "code"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Code, err = decodeString(m["code"], field(path, "code")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkPattern(v.Code, field(path, "code"), patternRedeemPairingCodeRequestCode); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeRegisterRequest(raw json.RawMessage, path string) (RegisterRequest, error) {
|
||||
var v RegisterRequest
|
||||
m, err := decodeObject(raw, path, "password", "username")
|
||||
|
||||
+60
-5
@@ -161,10 +161,11 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"username": {"type": "string"},
|
||||
"email": {"type": "string", "description": "账号设置了邮箱时才有"}
|
||||
"username": {"type": "string", "description": "设置了用户名和密码时才有,只绑定了 GitHub 的账号没有"},
|
||||
"email": {"type": "string", "description": "账号设置了邮箱时才有"},
|
||||
"github": {"type": "boolean", "description": "绑定了 GitHub"}
|
||||
},
|
||||
"required": ["username"]
|
||||
"required": ["github"]
|
||||
},
|
||||
"AdminAccount": {
|
||||
"type": "object",
|
||||
@@ -375,6 +376,11 @@
|
||||
"items": {"type": "array", "items": {"$ref": "#/$defs/MyPlaylistItem"}, "description": "新加入的在前"}
|
||||
},
|
||||
"required": ["name", "favorites", "ownerName", "ownerAvatarHash", "items"]
|
||||
},
|
||||
"PairingCode": {
|
||||
"type": "string",
|
||||
"description": "配对码:8 位,由数字 2–9 和除 I、O 以外的大写字母组成",
|
||||
"pattern": "^[2-9A-HJ-NP-Z]{8}$"
|
||||
}
|
||||
},
|
||||
"x-ws": {
|
||||
@@ -795,7 +801,7 @@
|
||||
},
|
||||
{
|
||||
"name": "register",
|
||||
"description": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN",
|
||||
"description": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制",
|
||||
"method": "POST",
|
||||
"path": "/api/account",
|
||||
"auth": "member",
|
||||
@@ -820,7 +826,7 @@
|
||||
},
|
||||
{
|
||||
"name": "changePassword",
|
||||
"description": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS",
|
||||
"description": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS,账号还没有设置密码时返回 CONFLICT",
|
||||
"method": "PUT",
|
||||
"path": "/api/account/password",
|
||||
"auth": "member",
|
||||
@@ -1686,6 +1692,55 @@
|
||||
"required": ["name"]
|
||||
},
|
||||
"response": {"$ref": "#/$defs/MyPlaylist"}
|
||||
},
|
||||
{
|
||||
"name": "createPairingCode",
|
||||
"description": "已注册的账号生成配对码,新设备兑换之后登录这个账号:5 分钟内有效,只能兑换一次,发出请求的成员 token 失效时随之作废;游客调用时返回 FORBIDDEN",
|
||||
"method": "POST",
|
||||
"path": "/api/account/pairing-codes",
|
||||
"auth": "member",
|
||||
"status": 201,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {"$ref": "#/$defs/PairingCode"},
|
||||
"qrImage": {"type": "string", "description": "内容为 ALISTEN_PUBLIC_URL/pair#<配对码> 的二维码,PNG 的 data: URI"},
|
||||
"expiresAtMs": {"type": "integer"}
|
||||
},
|
||||
"required": ["code", "qrImage", "expiresAtMs"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "redeemPairingCode",
|
||||
"description": "游客兑换配对码,登录生成它的账号:返回账号的新成员 token,发出请求的游客 token 随即失效;配对码不对或已过期时返回 INVALID_CREDENTIALS,已经登录了账号时返回 CONFLICT",
|
||||
"method": "POST",
|
||||
"path": "/api/sessions/pairing",
|
||||
"auth": "member",
|
||||
"status": 201,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {"$ref": "#/$defs/PairingCode"}
|
||||
},
|
||||
"required": ["code"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"memberToken": {"type": "string"},
|
||||
"member": {"$ref": "#/$defs/Member"},
|
||||
"account": {"$ref": "#/$defs/Account"}
|
||||
},
|
||||
"required": ["memberToken", "member", "account"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+156
-4
@@ -21,12 +21,17 @@
|
||||
"description": "账号设置了邮箱时才有",
|
||||
"type": "string"
|
||||
},
|
||||
"github": {
|
||||
"description": "绑定了 GitHub",
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"description": "设置了用户名和密码时才有,只绑定了 GitHub 的账号没有",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username"
|
||||
"github"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -523,6 +528,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreatePairingCodeRequest": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"CreatePairingCodeResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "配对码:8 位,由数字 2–9 和除 I、O 以外的大写字母组成",
|
||||
"pattern": "^[2-9A-HJ-NP-Z]{8}$",
|
||||
"type": "string"
|
||||
},
|
||||
"expiresAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"qrImage": {
|
||||
"description": "内容为 ALISTEN_PUBLIC_URL/pair#<配对码> 的二维码,PNG 的 data: URI",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code",
|
||||
"qrImage",
|
||||
"expiresAtMs"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateRoomRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1425,6 +1457,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RedeemPairingCodeRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "配对码:8 位,由数字 2–9 和除 I、O 以外的大写字母组成",
|
||||
"pattern": "^[2-9A-HJ-NP-Z]{8}$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RedeemPairingCodeResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
},
|
||||
"member": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
},
|
||||
"memberToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memberToken",
|
||||
"member",
|
||||
"account"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RegisterRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2033,7 +2099,7 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Alisten",
|
||||
"version": "cbfa923a9cbe9f0a"
|
||||
"version": "0023496d94e1b43f"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
@@ -2077,7 +2143,50 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN"
|
||||
"summary": "游客注册账号:设置用户名和密码,成员和成员 token 都不变;站点关闭注册时返回 FORBIDDEN。只绑定了 GitHub 的账号也用它补设用户名和密码,不受注册开关限制"
|
||||
}
|
||||
},
|
||||
"/api/account/pairing-codes": {
|
||||
"post": {
|
||||
"operationId": "createPairingCode",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePairingCodeRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePairingCodeResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "已注册的账号生成配对码,新设备兑换之后登录这个账号:5 分钟内有效,只能兑换一次,发出请求的成员 token 失效时随之作废;游客调用时返回 FORBIDDEN"
|
||||
}
|
||||
},
|
||||
"/api/account/password": {
|
||||
@@ -2113,7 +2222,7 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS"
|
||||
"summary": "修改密码:账号在其他设备上的成员 token 随即失效,发出请求的这个不变;当前密码错误时返回 INVALID_CREDENTIALS,账号还没有设置密码时返回 CONFLICT"
|
||||
}
|
||||
},
|
||||
"/api/admin/accounts/{username}": {
|
||||
@@ -4772,6 +4881,49 @@
|
||||
"summary": "退出登录:发出请求的成员 token 随即失效"
|
||||
}
|
||||
},
|
||||
"/api/sessions/pairing": {
|
||||
"post": {
|
||||
"operationId": "redeemPairingCode",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RedeemPairingCodeRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RedeemPairingCodeResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "游客兑换配对码,登录生成它的账号:返回账号的新成员 token,发出请求的游客 token 随即失效;配对码不对或已过期时返回 INVALID_CREDENTIALS,已经登录了账号时返回 CONFLICT"
|
||||
}
|
||||
},
|
||||
"/api/shared-playlists/{shareId}": {
|
||||
"get": {
|
||||
"operationId": "getSharedPlaylist",
|
||||
|
||||
+23
-2
@@ -4,7 +4,7 @@ package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const Version = "cbfa923a9cbe9f0a"
|
||||
const Version = "0023496d94e1b43f"
|
||||
|
||||
type APIKey struct {
|
||||
Key string `json:"key"`
|
||||
@@ -12,7 +12,8 @@ type APIKey struct {
|
||||
|
||||
type Account struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Github bool `json:"github"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
}
|
||||
|
||||
type ActivityKind string
|
||||
@@ -196,6 +197,14 @@ type CreateMyPlaylistRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreatePairingCodeRequest struct{}
|
||||
|
||||
type CreatePairingCodeResponse struct {
|
||||
Code string `json:"code"`
|
||||
ExpiresAtMs int64 `json:"expiresAtMs"`
|
||||
QrImage string `json:"qrImage"`
|
||||
}
|
||||
|
||||
type CreateRoomRequest struct {
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
@@ -598,6 +607,16 @@ func (v RecommendListResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type RedeemPairingCodeRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type RedeemPairingCodeResponse struct {
|
||||
Account Account `json:"account"`
|
||||
Member Member `json:"member"`
|
||||
MemberToken string `json:"memberToken"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
@@ -926,6 +945,8 @@ var (
|
||||
RouteUnshareMyPlaylist = Route{Pattern: "DELETE /api/me/playlists/{id}/share", Auth: AuthMember, Status: 204}
|
||||
RouteGetSharedPlaylist = Route{Pattern: "GET /api/shared-playlists/{shareId}", Auth: AuthNone, Status: 200}
|
||||
RouteCopySharedPlaylist = Route{Pattern: "POST /api/shared-playlists/{shareId}/copies", Auth: AuthMember, Status: 201}
|
||||
RouteCreatePairingCode = Route{Pattern: "POST /api/account/pairing-codes", Auth: AuthMember, Status: 201}
|
||||
RouteRedeemPairingCode = Route{Pattern: "POST /api/sessions/pairing", Auth: AuthMember, Status: 201}
|
||||
)
|
||||
|
||||
var RoomRoutes = []RoomRoute{
|
||||
|
||||
Reference in new issue
Block a user