mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-08-12 17:10:37 +00:00
Merge branch 'pr/225'
This commit is contained in:
commit
a8b45a8b8b
33
interface.d.ts
vendored
33
interface.d.ts
vendored
@ -626,6 +626,20 @@ export function event_del(
|
|||||||
params: { evId: string | number } & RequestBaseConfig,
|
params: { evId: string | number } & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export const enum EventPrivacy {
|
||||||
|
everyone = 0,
|
||||||
|
following = 1,
|
||||||
|
onlyMe = 2,
|
||||||
|
mutualFollowing = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function event_privacy(
|
||||||
|
params: {
|
||||||
|
evId: string | number
|
||||||
|
privacy: EventPrivacy
|
||||||
|
} & RequestBaseConfig,
|
||||||
|
): Promise<Response>
|
||||||
|
|
||||||
export function event_forward(
|
export function event_forward(
|
||||||
params: {
|
params: {
|
||||||
forwords: string
|
forwords: string
|
||||||
@ -1267,6 +1281,25 @@ export function user_event(
|
|||||||
} & RequestBaseConfig,
|
} & RequestBaseConfig,
|
||||||
): Promise<Response>
|
): Promise<Response>
|
||||||
|
|
||||||
|
export interface UserEventAllResponse {
|
||||||
|
code: number
|
||||||
|
events: Record<string, unknown>[]
|
||||||
|
/** Upstream account statistic; it can include events no longer returned. */
|
||||||
|
size: number | null
|
||||||
|
/** Number of unique events in `events`. */
|
||||||
|
retrievedCount: number
|
||||||
|
/** Positive difference between `size` and `retrievedCount`. */
|
||||||
|
unavailableCount: number | null
|
||||||
|
sizeMismatch: boolean | null
|
||||||
|
pageCount: number
|
||||||
|
more: false
|
||||||
|
lasttime: string | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function user_event_all(
|
||||||
|
params: RequestBaseConfig,
|
||||||
|
): Promise<Response<UserEventAllResponse>>
|
||||||
|
|
||||||
export function user_followeds(
|
export function user_followeds(
|
||||||
params: {
|
params: {
|
||||||
uid: string | number
|
uid: string | number
|
||||||
|
|||||||
34
module/event_privacy.js
Normal file
34
module/event_privacy.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 修改本人动态的可见权限
|
||||||
|
|
||||||
|
const createOption = require('../util/option.js')
|
||||||
|
|
||||||
|
const PRIVACY_VALUES = new Set([0, 1, 2, 6])
|
||||||
|
|
||||||
|
module.exports = (query, request) => {
|
||||||
|
const eventId = String(query.evId ?? '').trim()
|
||||||
|
const rawPrivacy = String(query.privacy ?? '').trim()
|
||||||
|
const privacy = Number(rawPrivacy)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!eventId ||
|
||||||
|
!rawPrivacy ||
|
||||||
|
!Number.isInteger(privacy) ||
|
||||||
|
!PRIVACY_VALUES.has(privacy)
|
||||||
|
) {
|
||||||
|
return Promise.resolve({
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
code: 400,
|
||||||
|
message: 'evId is required and privacy must be one of 0, 1, 2, 6',
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
eventId,
|
||||||
|
privacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(`/api/event/privacy/op`, data, createOption(query))
|
||||||
|
}
|
||||||
@ -4,9 +4,10 @@ const createOption = require('../util/option.js')
|
|||||||
module.exports = (query, request) => {
|
module.exports = (query, request) => {
|
||||||
const data = {
|
const data = {
|
||||||
getcounts: true,
|
getcounts: true,
|
||||||
time: query.lasttime || -1,
|
time: query.lasttime ?? -1,
|
||||||
limit: query.limit || 30,
|
limit: query.limit ?? 30,
|
||||||
total: false,
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
}
|
}
|
||||||
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
||||||
}
|
}
|
||||||
|
|||||||
135
module/user_event_all.js
Normal file
135
module/user_event_all.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 获取当前登录用户可被上游枚举的全部动态
|
||||||
|
|
||||||
|
const userAccount = require('./user_account.js')
|
||||||
|
const userEvent = require('./user_event.js')
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
const MAX_PAGES = 1000
|
||||||
|
|
||||||
|
const errorResponse = (message, cookie = []) => ({
|
||||||
|
status: 502,
|
||||||
|
body: {
|
||||||
|
code: 502,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
cookie,
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async (query, request) => {
|
||||||
|
const accountResult = await userAccount(query, request)
|
||||||
|
const uid =
|
||||||
|
accountResult.body?.account?.id || accountResult.body?.profile?.userId
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
if (
|
||||||
|
accountResult.status !== 200 ||
|
||||||
|
(accountResult.body?.code && accountResult.body.code !== 200)
|
||||||
|
) {
|
||||||
|
return accountResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 401,
|
||||||
|
body: {
|
||||||
|
code: 401,
|
||||||
|
message: 'A valid login cookie is required',
|
||||||
|
},
|
||||||
|
cookie: accountResult.cookie || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = [...(accountResult.cookie || [])]
|
||||||
|
const events = []
|
||||||
|
const eventIds = new Set()
|
||||||
|
const cursors = new Set()
|
||||||
|
let lasttime = -1
|
||||||
|
let more = true
|
||||||
|
let pageCount = 0
|
||||||
|
let size = null
|
||||||
|
|
||||||
|
while (more) {
|
||||||
|
pageCount += 1
|
||||||
|
|
||||||
|
const pageResult = await userEvent(
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
uid,
|
||||||
|
lasttime,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
cookies.push(...(pageResult.cookie || []))
|
||||||
|
|
||||||
|
if (pageResult.status !== 200 || pageResult.body?.code !== 200) {
|
||||||
|
return {
|
||||||
|
...pageResult,
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageCount === 1) {
|
||||||
|
const reportedSize = pageResult.body.size
|
||||||
|
const numericSize = Number(reportedSize)
|
||||||
|
size =
|
||||||
|
reportedSize != null && Number.isFinite(numericSize)
|
||||||
|
? numericSize
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of pageResult.body.events || []) {
|
||||||
|
if (event?.id == null) {
|
||||||
|
events.push(event)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = String(event.id)
|
||||||
|
if (!eventIds.has(eventId)) {
|
||||||
|
eventIds.add(eventId)
|
||||||
|
events.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
more = Boolean(pageResult.body.more)
|
||||||
|
lasttime = pageResult.body.lasttime
|
||||||
|
|
||||||
|
if (more) {
|
||||||
|
const cursor = String(lasttime ?? '')
|
||||||
|
if (!cursor || cursors.has(cursor)) {
|
||||||
|
return errorResponse(
|
||||||
|
'Upstream event pagination cursor stalled',
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cursors.add(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (more && pageCount >= MAX_PAGES) {
|
||||||
|
return errorResponse(
|
||||||
|
`Upstream event pagination exceeded ${MAX_PAGES} pages`,
|
||||||
|
cookies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrievedCount = events.length
|
||||||
|
const unavailableCount =
|
||||||
|
size == null ? null : Math.max(size - retrievedCount, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events,
|
||||||
|
size,
|
||||||
|
retrievedCount,
|
||||||
|
unavailableCount,
|
||||||
|
sizeMismatch: size == null ? null : size !== retrievedCount,
|
||||||
|
pageCount,
|
||||||
|
more: false,
|
||||||
|
lasttime: lasttime ?? null,
|
||||||
|
},
|
||||||
|
cookie: cookies,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -826,6 +826,8 @@ tags: 歌单标签
|
|||||||
|
|
||||||
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
|
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
|
||||||
|
|
||||||
|
接口会传入官方客户端使用的 `fromRN=true`,登录用户读取自己的动态时可获得网易云允许本人查看的非公开动态。返回的 `size` 是网易云账户侧统计值,不保证等于分页后实际可获取的动态数量。
|
||||||
|
|
||||||
**接口地址 :** `/user/event`
|
**接口地址 :** `/user/event`
|
||||||
|
|
||||||
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
|
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
|
||||||
@ -843,6 +845,38 @@ tags: 歌单标签
|
|||||||
41、21 分享视频
|
41、21 分享视频
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 获取当前登录用户的全部可枚举动态
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口,会组合 `/user/account` 与 `/user/event` 的原子能力:先从 Cookie 取得当前用户 id,再自动跟随 `lasttime` 游标读取至 `more=false`。接口返回网易云允许当前用户本人查看的公开及非公开动态,不能读取其他用户的私密动态。
|
||||||
|
|
||||||
|
本接口不接受 `limit` 或 `lasttime`,一次请求会完成全部上游分页。账号动态较多时,请预留足够的请求时间。
|
||||||
|
|
||||||
|
数量字段说明:
|
||||||
|
|
||||||
|
- `size`:网易云返回的账户统计数量,可能包含已删除、被屏蔽、资源失效或旧类型且不再下发的记录。
|
||||||
|
- `retrievedCount`:本次实际取得的唯一动态数量,始终等于 `events.length`。
|
||||||
|
- `unavailableCount`:`size - retrievedCount` 的正差值;缺失记录没有返回 id,无法继续读取或修改。
|
||||||
|
- `sizeMismatch`:`size` 与 `retrievedCount` 是否不一致。若上游未返回 `size`,相关字段为 `null`。
|
||||||
|
- `pageCount`:本次实际请求的上游分页数量。
|
||||||
|
|
||||||
|
每条动态的 `privacySetting` 表示当前可见权限:`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人。
|
||||||
|
|
||||||
|
**接口地址 :** `/user/event/all`
|
||||||
|
|
||||||
|
**调用例子 :** `/user/event/all`
|
||||||
|
|
||||||
|
### 修改动态可见权限
|
||||||
|
|
||||||
|
说明 : 登录后调用此接口,可以修改当前账号本人发布的单条动态的可见权限。此接口只负责一次原子修改;上游没有批量修改接口。如需批量操作,可先调用 `/user/event/all` 并按 `privacySetting` 筛选,再由调用方逐条调用本接口,同时自行处理限速、失败重试和部分成功。
|
||||||
|
|
||||||
|
**必选参数 :** `evId` : 动态 id
|
||||||
|
|
||||||
|
`privacy` : 目标可见权限。`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人
|
||||||
|
|
||||||
|
**接口地址 :** `/event/privacy`
|
||||||
|
|
||||||
|
**调用例子 :** `/event/privacy?evId=6712917601&privacy=0`
|
||||||
|
|
||||||
### 转发用户动态
|
### 转发用户动态
|
||||||
|
|
||||||
说明 : 登录后调用此接口 ,可以转发用户动态
|
说明 : 登录后调用此接口 ,可以转发用户动态
|
||||||
|
|||||||
34
test/event_privacy.test.js
Normal file
34
test/event_privacy.test.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const eventPrivacy = require('../module/event_privacy')
|
||||||
|
|
||||||
|
describe('event privacy modules', () => {
|
||||||
|
it('updates event privacy with the official client parameters', async () => {
|
||||||
|
let captured
|
||||||
|
const expected = { status: 200, body: { code: 200 }, cookie: [] }
|
||||||
|
const result = await eventPrivacy(
|
||||||
|
{ evId: '123456789012345678', privacy: '0', cookie: { MUSIC_U: 'x' } },
|
||||||
|
async (uri, data, options) => {
|
||||||
|
captured = { uri, data, options }
|
||||||
|
return expected
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(result, expected)
|
||||||
|
assert.strictEqual(captured.uri, '/api/event/privacy/op')
|
||||||
|
assert.deepStrictEqual(captured.data, {
|
||||||
|
eventId: '123456789012345678',
|
||||||
|
privacy: 0,
|
||||||
|
})
|
||||||
|
assert.strictEqual(captured.options.crypto, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects unknown event privacy values before making a request', async () => {
|
||||||
|
let requested = false
|
||||||
|
const result = await eventPrivacy({ evId: '1', privacy: '3' }, async () => {
|
||||||
|
requested = true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.strictEqual(requested, false)
|
||||||
|
assert.strictEqual(result.status, 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
145
test/user_event_all.test.js
Normal file
145
test/user_event_all.test.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const userEventAll = require('../module/user_event_all')
|
||||||
|
|
||||||
|
describe('all current user events module', () => {
|
||||||
|
it('resolves the current user and aggregates every upstream page', async () => {
|
||||||
|
const calls = []
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events: [{ id: 1 }, { id: 2 }],
|
||||||
|
size: 5,
|
||||||
|
more: true,
|
||||||
|
lasttime: 100,
|
||||||
|
},
|
||||||
|
cookie: ['page=1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events: [{ id: 2 }, { id: 3 }],
|
||||||
|
size: 5,
|
||||||
|
more: true,
|
||||||
|
lasttime: 50,
|
||||||
|
},
|
||||||
|
cookie: ['page=2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events: [{ id: 4 }],
|
||||||
|
size: 1,
|
||||||
|
more: false,
|
||||||
|
lasttime: 0,
|
||||||
|
},
|
||||||
|
cookie: ['page=3'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = await userEventAll(
|
||||||
|
{ cookie: { MUSIC_U: 'x' } },
|
||||||
|
async (uri, data, options) => {
|
||||||
|
calls.push({ uri, data, options })
|
||||||
|
if (uri === '/api/nuser/account/get') {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, account: { id: 42 } },
|
||||||
|
cookie: ['account=1'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages.shift()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
result.body.events.map((event) => event.id),
|
||||||
|
[1, 2, 3, 4],
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.body.size, 5)
|
||||||
|
assert.strictEqual(result.body.retrievedCount, 4)
|
||||||
|
assert.strictEqual(result.body.unavailableCount, 1)
|
||||||
|
assert.strictEqual(result.body.sizeMismatch, true)
|
||||||
|
assert.strictEqual(result.body.pageCount, 3)
|
||||||
|
assert.strictEqual(result.body.more, false)
|
||||||
|
assert.deepStrictEqual(result.cookie, [
|
||||||
|
'account=1',
|
||||||
|
'page=1',
|
||||||
|
'page=2',
|
||||||
|
'page=3',
|
||||||
|
])
|
||||||
|
|
||||||
|
assert.strictEqual(calls[0].uri, '/api/nuser/account/get')
|
||||||
|
assert.strictEqual(calls[0].options.crypto, 'weapi')
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
calls.slice(1).map((call) => call.data),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
getcounts: true,
|
||||||
|
time: -1,
|
||||||
|
limit: 100,
|
||||||
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getcounts: true,
|
||||||
|
time: 100,
|
||||||
|
limit: 100,
|
||||||
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getcounts: true,
|
||||||
|
time: 50,
|
||||||
|
limit: 100,
|
||||||
|
total: false,
|
||||||
|
fromRN: 'true',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires a valid login cookie', async () => {
|
||||||
|
const result = await userEventAll({}, async () => ({
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, account: null, profile: null },
|
||||||
|
cookie: [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 401)
|
||||||
|
assert.strictEqual(result.body.code, 401)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails instead of returning a partial list when the cursor stalls', async () => {
|
||||||
|
let page = 0
|
||||||
|
const result = await userEventAll({}, async (uri) => {
|
||||||
|
if (uri === '/api/nuser/account/get') {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: { code: 200, account: { id: 42 } },
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
code: 200,
|
||||||
|
events: [],
|
||||||
|
size: 1,
|
||||||
|
more: true,
|
||||||
|
lasttime: 100,
|
||||||
|
},
|
||||||
|
cookie: [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.strictEqual(page, 2)
|
||||||
|
assert.strictEqual(result.status, 502)
|
||||||
|
assert.match(result.body.message, /cursor stalled/)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user