feat: remove, copy and reorder several playlist songs at once
This commit is contained in:
1 parent
d9a97b6331
commit
5c34450ccf
30 files changed
+1482
-74
No files matched your search
+73
-7
@@ -98,8 +98,16 @@ func DecodeAddMyPlaylistItemRequest(body []byte) (AddMyPlaylistItemRequest, erro
|
||||
return decodeAddMyPlaylistItemRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeRemoveMyPlaylistItemRequest(body []byte) (RemoveMyPlaylistItemRequest, error) {
|
||||
return decodeRemoveMyPlaylistItemRequest(body, "")
|
||||
func DecodeRemoveMyPlaylistItemsRequest(body []byte) (RemoveMyPlaylistItemsRequest, error) {
|
||||
return decodeRemoveMyPlaylistItemsRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeCopyMyPlaylistItemsRequest(body []byte) (CopyMyPlaylistItemsRequest, error) {
|
||||
return decodeCopyMyPlaylistItemsRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeMoveMyPlaylistItemsRequest(body []byte) (MoveMyPlaylistItemsRequest, error) {
|
||||
return decodeMoveMyPlaylistItemsRequest(body, "")
|
||||
}
|
||||
|
||||
func DecodeCreateRoomRequest(body []byte) (CreateRoomRequest, error) {
|
||||
@@ -450,6 +458,30 @@ func decodeChatSendRequest(raw json.RawMessage, path string) (ChatSendRequest, e
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeCopyMyPlaylistItemsRequest(raw json.RawMessage, path string) (CopyMyPlaylistItemsRequest, error) {
|
||||
var v CopyMyPlaylistItemsRequest
|
||||
m, err := decodeObject(raw, path, "from", "tracks")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "from", "tracks"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.From, err = decodeString(m["from"], field(path, "from")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Tracks, err = decodeArray(decodeTrack)(m["tracks"], field(path, "tracks")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinItems(v.Tracks, field(path, "tracks"), 1); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxItems(v.Tracks, field(path, "tracks"), 1000); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeCreateMemberRequest(raw json.RawMessage, path string) (CreateMemberRequest, error) {
|
||||
var v CreateMemberRequest
|
||||
_, err := decodeObject(raw, path)
|
||||
@@ -572,6 +604,34 @@ func decodeLoginRequest(raw json.RawMessage, path string) (LoginRequest, error)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeMoveMyPlaylistItemsRequest(raw json.RawMessage, path string) (MoveMyPlaylistItemsRequest, error) {
|
||||
var v MoveMyPlaylistItemsRequest
|
||||
m, err := decodeObject(raw, path, "before", "tracks")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "tracks"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if r, ok := m["before"]; ok {
|
||||
x, err := decodeTrack(r, field(path, "before"))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.Before = &x
|
||||
}
|
||||
if v.Tracks, err = decodeArray(decodeTrack)(m["tracks"], field(path, "tracks")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinItems(v.Tracks, field(path, "tracks"), 1); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxItems(v.Tracks, field(path, "tracks"), 1000); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodePingRequest(raw json.RawMessage, path string) (PingRequest, error) {
|
||||
var v PingRequest
|
||||
m, err := decodeObject(raw, path, "clientTimeMs")
|
||||
@@ -750,16 +810,22 @@ func decodeRegisterRequest(raw json.RawMessage, path string) (RegisterRequest, e
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeRemoveMyPlaylistItemRequest(raw json.RawMessage, path string) (RemoveMyPlaylistItemRequest, error) {
|
||||
var v RemoveMyPlaylistItemRequest
|
||||
m, err := decodeObject(raw, path, "track")
|
||||
func decodeRemoveMyPlaylistItemsRequest(raw json.RawMessage, path string) (RemoveMyPlaylistItemsRequest, error) {
|
||||
var v RemoveMyPlaylistItemsRequest
|
||||
m, err := decodeObject(raw, path, "tracks")
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = requireFields(m, path, "track"); err != nil {
|
||||
if err = requireFields(m, path, "tracks"); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if v.Track, err = decodeTrack(m["track"], field(path, "track")); err != nil {
|
||||
if v.Tracks, err = decodeArray(decodeTrack)(m["tracks"], field(path, "tracks")); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMinItems(v.Tracks, field(path, "tracks"), 1); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if err = checkMaxItems(v.Tracks, field(path, "tracks"), 1000); err != nil {
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
|
||||
@@ -88,6 +88,38 @@ func decodeInteger(raw json.RawMessage, path string) (int64, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func decodeArray[T any](decode func(json.RawMessage, string) (T, error)) func(json.RawMessage, string) ([]T, error) {
|
||||
return func(raw json.RawMessage, path string) ([]T, error) {
|
||||
var elems []json.RawMessage
|
||||
if bytes.Equal(raw, null) || json.Unmarshal(raw, &elems) != nil {
|
||||
return nil, invalid(path, "应为数组")
|
||||
}
|
||||
out := make([]T, len(elems))
|
||||
for i, e := range elems {
|
||||
v, err := decode(e, fmt.Sprintf("%s[%d]", path, i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkMinItems[T any](a []T, path string, n int) error {
|
||||
if len(a) < n {
|
||||
return invalid(path, fmt.Sprintf("元素不能少于 %d 个", n))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMaxItems[T any](a []T, path string, n int) error {
|
||||
if len(a) > n {
|
||||
return invalid(path, fmt.Sprintf("元素不能超过 %d 个", n))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMinLength(s, path string, n int) error {
|
||||
if utf8.RuneCountInString(s) < n {
|
||||
return invalid(path, fmt.Sprintf("长度不能少于 %d 个字符", n))
|
||||
|
||||
@@ -3,6 +3,7 @@ package protocol
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -67,3 +68,27 @@ func TestCheckOneOf(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTrackArray(t *testing.T) {
|
||||
one := `{"source":"netease","id":"1"}`
|
||||
tests := []struct {
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{`{"tracks":[` + one + `]}`, ""},
|
||||
{`{"tracks":[]}`, "tracks:元素不能少于 1 个"},
|
||||
{`{"tracks":null}`, "tracks:应为数组"},
|
||||
{`{"tracks":[{"source":"netease"}]}`, "tracks[0].id:缺少必填字段"},
|
||||
{`{}`, "tracks:缺少必填字段"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := DecodeRemoveMyPlaylistItemsRequest([]byte(tt.body))
|
||||
if got := errText(err); got != tt.want {
|
||||
t.Errorf("%s = %q,期望 %q", tt.body, got, tt.want)
|
||||
}
|
||||
}
|
||||
tooMany := `{"tracks":[` + one + strings.Repeat(","+one, 1000) + `]}`
|
||||
if got := errText(func() error { _, err := DecodeRemoveMyPlaylistItemsRequest([]byte(tooMany)); return err }()); got != "tracks:元素不能超过 1000 个" {
|
||||
t.Errorf("1001 首 = %q", got)
|
||||
}
|
||||
}
|
||||
+45
-4
@@ -923,8 +923,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "removeMyPlaylistItem",
|
||||
"description": "把一首歌移出歌单;不在歌单里也算成功",
|
||||
"name": "removeMyPlaylistItems",
|
||||
"description": "把若干首歌移出歌单,一次 1 到 1000 首;不在歌单里的忽略",
|
||||
"method": "POST",
|
||||
"path": "/api/me/playlists/{id}/removals",
|
||||
"auth": "member",
|
||||
@@ -933,9 +933,50 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"track": {"$ref": "#/$defs/Track"}
|
||||
"tracks": {"type": "array", "items": {"$ref": "#/$defs/Track"}, "minItems": 1, "maxItems": 1000}
|
||||
},
|
||||
"required": ["track"]
|
||||
"required": ["tracks"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "copyMyPlaylistItems",
|
||||
"description": "把另一个歌单里的若干首歌连同元数据复制到这个歌单,一次 1 到 1000 首;不在来源歌单里的和已在这个歌单里的跳过;新复制的排在最前,保持 tracks 的先后;装不下时整批拒绝;返回实际复制的数量",
|
||||
"method": "POST",
|
||||
"path": "/api/me/playlists/{id}/copies",
|
||||
"auth": "member",
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": {"type": "string", "description": "来源歌单的 ID"},
|
||||
"tracks": {"type": "array", "items": {"$ref": "#/$defs/Track"}, "minItems": 1, "maxItems": 1000}
|
||||
},
|
||||
"required": ["from", "tracks"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"added": {"type": "integer"}
|
||||
},
|
||||
"required": ["added"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "moveMyPlaylistItems",
|
||||
"description": "调整歌单里的顺序:把 tracks 里在歌单中的歌按给出的先后放到 before 之前,不给 before 就放到最后;一次 1 到 1000 首;before 不能在 tracks 里,也必须还在歌单里",
|
||||
"method": "POST",
|
||||
"path": "/api/me/playlists/{id}/moves",
|
||||
"auth": "member",
|
||||
"status": 204,
|
||||
"request": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tracks": {"type": "array", "items": {"$ref": "#/$defs/Track"}, "minItems": 1, "maxItems": 1000},
|
||||
"before": {"$ref": "#/$defs/Track"}
|
||||
},
|
||||
"required": ["tracks"]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+167
-9
@@ -441,6 +441,40 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CopyMyPlaylistItemsRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": {
|
||||
"description": "来源歌单的 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"tracks": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Track"
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from",
|
||||
"tracks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CopyMyPlaylistItemsResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"added": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"added"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateMemberRequest": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
@@ -806,6 +840,26 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MoveMyPlaylistItemsRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"before": {
|
||||
"$ref": "#/components/schemas/Track"
|
||||
},
|
||||
"tracks": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Track"
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tracks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MyPlaylist": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1178,15 +1232,20 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RemoveMyPlaylistItemRequest": {
|
||||
"RemoveMyPlaylistItemsRequest": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"track": {
|
||||
"$ref": "#/components/schemas/Track"
|
||||
"tracks": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Track"
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"track"
|
||||
"tracks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -1655,7 +1714,7 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Alisten",
|
||||
"version": "1132502d511e1bab"
|
||||
"version": "236deb132b07fa9e"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
@@ -2946,6 +3005,59 @@
|
||||
"summary": "重命名歌单;“我喜欢”不能重命名"
|
||||
}
|
||||
},
|
||||
"/api/me/playlists/{id}/copies": {
|
||||
"post": {
|
||||
"operationId": "copyMyPlaylistItems",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CopyMyPlaylistItemsRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CopyMyPlaylistItemsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "把另一个歌单里的若干首歌连同元数据复制到这个歌单,一次 1 到 1000 首;不在来源歌单里的和已在这个歌单里的跳过;新复制的排在最前,保持 tracks 的先后;装不下时整批拒绝;返回实际复制的数量"
|
||||
}
|
||||
},
|
||||
"/api/me/playlists/{id}/items": {
|
||||
"get": {
|
||||
"operationId": "listMyPlaylistItems",
|
||||
@@ -3040,9 +3152,9 @@
|
||||
"summary": "把一首歌加入歌单:元数据由服务端查询,链接曲目要先准备好;已经在歌单里时不变;一个歌单最多 1000 首"
|
||||
}
|
||||
},
|
||||
"/api/me/playlists/{id}/removals": {
|
||||
"/api/me/playlists/{id}/moves": {
|
||||
"post": {
|
||||
"operationId": "removeMyPlaylistItem",
|
||||
"operationId": "moveMyPlaylistItems",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
@@ -3057,7 +3169,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RemoveMyPlaylistItemRequest"
|
||||
"$ref": "#/components/schemas/MoveMyPlaylistItemsRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3083,7 +3195,53 @@
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "把一首歌移出歌单;不在歌单里也算成功"
|
||||
"summary": "调整歌单里的顺序:把 tracks 里在歌单中的歌按给出的先后放到 before 之前,不给 before 就放到最后;一次 1 到 1000 首;before 不能在 tracks 里,也必须还在歌单里"
|
||||
}
|
||||
},
|
||||
"/api/me/playlists/{id}/removals": {
|
||||
"post": {
|
||||
"operationId": "removeMyPlaylistItems",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RemoveMyPlaylistItemsRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "成功"
|
||||
},
|
||||
"default": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "失败"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"memberToken": []
|
||||
}
|
||||
],
|
||||
"summary": "把若干首歌移出歌单,一次 1 到 1000 首;不在歌单里的忽略"
|
||||
}
|
||||
},
|
||||
"/api/members": {
|
||||
|
||||
+44
-4
@@ -4,7 +4,7 @@ package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const Version = "1132502d511e1bab"
|
||||
const Version = "236deb132b07fa9e"
|
||||
|
||||
type APIKey struct {
|
||||
Key string `json:"key"`
|
||||
@@ -164,6 +164,23 @@ type ChatSendRequest struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type CopyMyPlaylistItemsRequest struct {
|
||||
From string `json:"from"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
func (v CopyMyPlaylistItemsRequest) MarshalJSON() ([]byte, error) {
|
||||
type plain CopyMyPlaylistItemsRequest
|
||||
if v.Tracks == nil {
|
||||
v.Tracks = []Track{}
|
||||
}
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type CopyMyPlaylistItemsResponse struct {
|
||||
Added int64 `json:"added"`
|
||||
}
|
||||
|
||||
type CreateMemberRequest struct{}
|
||||
|
||||
type CreateMemberResponse struct {
|
||||
@@ -323,6 +340,19 @@ func (v Members) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type MoveMyPlaylistItemsRequest struct {
|
||||
Before *Track `json:"before,omitempty"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
func (v MoveMyPlaylistItemsRequest) MarshalJSON() ([]byte, error) {
|
||||
type plain MoveMyPlaylistItemsRequest
|
||||
if v.Tracks == nil {
|
||||
v.Tracks = []Track{}
|
||||
}
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type MyPlaylist struct {
|
||||
CoverURL *string `json:"coverUrl"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
@@ -494,8 +524,16 @@ type RegisterResponse struct {
|
||||
Account Account `json:"account"`
|
||||
}
|
||||
|
||||
type RemoveMyPlaylistItemRequest struct {
|
||||
Track Track `json:"track"`
|
||||
type RemoveMyPlaylistItemsRequest struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
func (v RemoveMyPlaylistItemsRequest) MarshalJSON() ([]byte, error) {
|
||||
type plain RemoveMyPlaylistItemsRequest
|
||||
if v.Tracks == nil {
|
||||
v.Tracks = []Track{}
|
||||
}
|
||||
return json.Marshal(plain(v))
|
||||
}
|
||||
|
||||
type RenameMyPlaylistRequest struct {
|
||||
@@ -724,7 +762,9 @@ var (
|
||||
RouteDeleteMyPlaylist = Route{Pattern: "DELETE /api/me/playlists/{id}", Auth: AuthMember, Status: 204}
|
||||
RouteListMyPlaylistItems = Route{Pattern: "GET /api/me/playlists/{id}/items", Auth: AuthMember, Status: 200}
|
||||
RouteAddMyPlaylistItem = Route{Pattern: "POST /api/me/playlists/{id}/items", Auth: AuthMember, Status: 201}
|
||||
RouteRemoveMyPlaylistItem = Route{Pattern: "POST /api/me/playlists/{id}/removals", Auth: AuthMember, Status: 204}
|
||||
RouteRemoveMyPlaylistItems = Route{Pattern: "POST /api/me/playlists/{id}/removals", Auth: AuthMember, Status: 204}
|
||||
RouteCopyMyPlaylistItems = Route{Pattern: "POST /api/me/playlists/{id}/copies", Auth: AuthMember, Status: 200}
|
||||
RouteMoveMyPlaylistItems = Route{Pattern: "POST /api/me/playlists/{id}/moves", Auth: AuthMember, Status: 204}
|
||||
RouteListRooms = Route{Pattern: "GET /api/rooms", Auth: AuthNone, Status: 200}
|
||||
RouteGetRoom = Route{Pattern: "GET /api/rooms/{ref}", Auth: AuthNone, Status: 200}
|
||||
RouteCreateRoom = Route{Pattern: "POST /api/rooms", Auth: AuthNone, Status: 201, Location: true}
|
||||
|
||||
Reference in new issue
Block a user