aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/server-commands/users
diff options
context:
space:
mode:
Diffstat (limited to 'shared/server-commands/users')
-rw-r--r--shared/server-commands/users/accounts-command.ts78
-rw-r--r--shared/server-commands/users/actors.ts73
-rw-r--r--shared/server-commands/users/blocklist-command.ts162
-rw-r--r--shared/server-commands/users/index.ts9
-rw-r--r--shared/server-commands/users/login-command.ts132
-rw-r--r--shared/server-commands/users/login.ts19
-rw-r--r--shared/server-commands/users/notifications-command.ts86
-rw-r--r--shared/server-commands/users/notifications.ts795
-rw-r--r--shared/server-commands/users/subscriptions-command.ts99
-rw-r--r--shared/server-commands/users/users-command.ts416
10 files changed, 1869 insertions, 0 deletions
diff --git a/shared/server-commands/users/accounts-command.ts b/shared/server-commands/users/accounts-command.ts
new file mode 100644
index 000000000..98d9d5927
--- /dev/null
+++ b/shared/server-commands/users/accounts-command.ts
@@ -0,0 +1,78 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { Account, ActorFollow } from '../../models/actors'
3import { AccountVideoRate, VideoRateType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class AccountsCommand extends AbstractCommand {
7
8 list (options: OverrideCommandOptions & {
9 sort?: string // default -createdAt
10 } = {}) {
11 const { sort = '-createdAt' } = options
12 const path = '/api/v1/accounts'
13
14 return this.getRequestBody<ResultList<Account>>({
15 ...options,
16
17 path,
18 query: { sort },
19 implicitToken: false,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 })
22 }
23
24 get (options: OverrideCommandOptions & {
25 accountName: string
26 }) {
27 const path = '/api/v1/accounts/' + options.accountName
28
29 return this.getRequestBody<Account>({
30 ...options,
31
32 path,
33 implicitToken: false,
34 defaultExpectedStatus: HttpStatusCode.OK_200
35 })
36 }
37
38 listRatings (options: OverrideCommandOptions & {
39 accountName: string
40 rating?: VideoRateType
41 }) {
42 const { rating, accountName } = options
43 const path = '/api/v1/accounts/' + accountName + '/ratings'
44
45 const query = { rating }
46
47 return this.getRequestBody<ResultList<AccountVideoRate>>({
48 ...options,
49
50 path,
51 query,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 listFollowers (options: OverrideCommandOptions & {
58 accountName: string
59 start?: number
60 count?: number
61 sort?: string
62 search?: string
63 }) {
64 const { accountName, start, count, sort, search } = options
65 const path = '/api/v1/accounts/' + accountName + '/followers'
66
67 const query = { start, count, sort, search }
68
69 return this.getRequestBody<ResultList<ActorFollow>>({
70 ...options,
71
72 path,
73 query,
74 implicitToken: true,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78}
diff --git a/shared/server-commands/users/actors.ts b/shared/server-commands/users/actors.ts
new file mode 100644
index 000000000..12c3e078a
--- /dev/null
+++ b/shared/server-commands/users/actors.ts
@@ -0,0 +1,73 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@shared/core-utils'
7import { Account, VideoChannel } from '@shared/models'
8import { PeerTubeServer } from '../server'
9
10async function expectChannelsFollows (options: {
11 server: PeerTubeServer
12 handle: string
13 followers: number
14 following: number
15}) {
16 const { server } = options
17 const { data } = await server.channels.list()
18
19 return expectActorFollow({ ...options, data })
20}
21
22async function expectAccountFollows (options: {
23 server: PeerTubeServer
24 handle: string
25 followers: number
26 following: number
27}) {
28 const { server } = options
29 const { data } = await server.accounts.list()
30
31 return expectActorFollow({ ...options, data })
32}
33
34async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
35 const testDirectory = 'test' + serverNumber
36
37 for (const directory of [ 'avatars' ]) {
38 const directoryPath = join(root(), testDirectory, directory)
39
40 const directoryExists = await pathExists(directoryPath)
41 expect(directoryExists).to.be.true
42
43 const files = await readdir(directoryPath)
44 for (const file of files) {
45 expect(file).to.not.contain(filename)
46 }
47 }
48}
49
50export {
51 expectAccountFollows,
52 expectChannelsFollows,
53 checkActorFilesWereRemoved
54}
55
56// ---------------------------------------------------------------------------
57
58function expectActorFollow (options: {
59 server: PeerTubeServer
60 data: (Account | VideoChannel)[]
61 handle: string
62 followers: number
63 following: number
64}) {
65 const { server, data, handle, followers, following } = options
66
67 const actor = data.find(a => a.name + '@' + a.host === handle)
68 const message = `${handle} on ${server.url}`
69
70 expect(actor, message).to.exist
71 expect(actor.followersCount).to.equal(followers, message)
72 expect(actor.followingCount).to.equal(following, message)
73}
diff --git a/shared/server-commands/users/blocklist-command.ts b/shared/server-commands/users/blocklist-command.ts
new file mode 100644
index 000000000..2e7ed074d
--- /dev/null
+++ b/shared/server-commands/users/blocklist-command.ts
@@ -0,0 +1,162 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6type ListBlocklistOptions = OverrideCommandOptions & {
7 start: number
8 count: number
9 sort: string // default -createdAt
10}
11
12export class BlocklistCommand extends AbstractCommand {
13
14 listMyAccountBlocklist (options: ListBlocklistOptions) {
15 const path = '/api/v1/users/me/blocklist/accounts'
16
17 return this.listBlocklist<AccountBlock>(options, path)
18 }
19
20 listMyServerBlocklist (options: ListBlocklistOptions) {
21 const path = '/api/v1/users/me/blocklist/servers'
22
23 return this.listBlocklist<ServerBlock>(options, path)
24 }
25
26 listServerAccountBlocklist (options: ListBlocklistOptions) {
27 const path = '/api/v1/server/blocklist/accounts'
28
29 return this.listBlocklist<AccountBlock>(options, path)
30 }
31
32 listServerServerBlocklist (options: ListBlocklistOptions) {
33 const path = '/api/v1/server/blocklist/servers'
34
35 return this.listBlocklist<ServerBlock>(options, path)
36 }
37
38 // ---------------------------------------------------------------------------
39
40 getStatus (options: OverrideCommandOptions & {
41 accounts?: string[]
42 hosts?: string[]
43 }) {
44 const { accounts, hosts } = options
45
46 const path = '/api/v1/blocklist/status'
47
48 return this.getRequestBody<BlockStatus>({
49 ...options,
50
51 path,
52 query: {
53 accounts,
54 hosts
55 },
56 implicitToken: false,
57 defaultExpectedStatus: HttpStatusCode.OK_200
58 })
59 }
60
61 // ---------------------------------------------------------------------------
62
63 addToMyBlocklist (options: OverrideCommandOptions & {
64 account?: string
65 server?: string
66 }) {
67 const { account, server } = options
68
69 const path = account
70 ? '/api/v1/users/me/blocklist/accounts'
71 : '/api/v1/users/me/blocklist/servers'
72
73 return this.postBodyRequest({
74 ...options,
75
76 path,
77 fields: {
78 accountName: account,
79 host: server
80 },
81 implicitToken: true,
82 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
83 })
84 }
85
86 addToServerBlocklist (options: OverrideCommandOptions & {
87 account?: string
88 server?: string
89 }) {
90 const { account, server } = options
91
92 const path = account
93 ? '/api/v1/server/blocklist/accounts'
94 : '/api/v1/server/blocklist/servers'
95
96 return this.postBodyRequest({
97 ...options,
98
99 path,
100 fields: {
101 accountName: account,
102 host: server
103 },
104 implicitToken: true,
105 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
106 })
107 }
108
109 // ---------------------------------------------------------------------------
110
111 removeFromMyBlocklist (options: OverrideCommandOptions & {
112 account?: string
113 server?: string
114 }) {
115 const { account, server } = options
116
117 const path = account
118 ? '/api/v1/users/me/blocklist/accounts/' + account
119 : '/api/v1/users/me/blocklist/servers/' + server
120
121 return this.deleteRequest({
122 ...options,
123
124 path,
125 implicitToken: true,
126 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
127 })
128 }
129
130 removeFromServerBlocklist (options: OverrideCommandOptions & {
131 account?: string
132 server?: string
133 }) {
134 const { account, server } = options
135
136 const path = account
137 ? '/api/v1/server/blocklist/accounts/' + account
138 : '/api/v1/server/blocklist/servers/' + server
139
140 return this.deleteRequest({
141 ...options,
142
143 path,
144 implicitToken: true,
145 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
146 })
147 }
148
149 private listBlocklist <T> (options: ListBlocklistOptions, path: string) {
150 const { start, count, sort = '-createdAt' } = options
151
152 return this.getRequestBody<ResultList<T>>({
153 ...options,
154
155 path,
156 query: { start, count, sort },
157 implicitToken: true,
158 defaultExpectedStatus: HttpStatusCode.OK_200
159 })
160 }
161
162}
diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts
new file mode 100644
index 000000000..460a06f70
--- /dev/null
+++ b/shared/server-commands/users/index.ts
@@ -0,0 +1,9 @@
1export * from './accounts-command'
2export * from './actors'
3export * from './blocklist-command'
4export * from './login'
5export * from './login-command'
6export * from './notifications'
7export * from './notifications-command'
8export * from './subscriptions-command'
9export * from './users-command'
diff --git a/shared/server-commands/users/login-command.ts b/shared/server-commands/users/login-command.ts
new file mode 100644
index 000000000..143f72a59
--- /dev/null
+++ b/shared/server-commands/users/login-command.ts
@@ -0,0 +1,132 @@
1import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models'
2import { unwrapBody } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class LoginCommand extends AbstractCommand {
6
7 login (options: OverrideCommandOptions & {
8 client?: { id?: string, secret?: string }
9 user?: { username: string, password?: string }
10 } = {}) {
11 const { client = this.server.store.client, user = this.server.store.user } = options
12 const path = '/api/v1/users/token'
13
14 const body = {
15 client_id: client.id,
16 client_secret: client.secret,
17 username: user.username,
18 password: user.password ?? 'password',
19 response_type: 'code',
20 grant_type: 'password',
21 scope: 'upload'
22 }
23
24 return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({
25 ...options,
26
27 path,
28 requestType: 'form',
29 fields: body,
30 implicitToken: false,
31 defaultExpectedStatus: HttpStatusCode.OK_200
32 }))
33 }
34
35 getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
36 getAccessToken (arg1: string, password?: string): Promise<string>
37 async getAccessToken (arg1?: { username: string, password?: string } | string, password?: string) {
38 let user: { username: string, password?: string }
39
40 if (!arg1) user = this.server.store.user
41 else if (typeof arg1 === 'object') user = arg1
42 else user = { username: arg1, password }
43
44 try {
45 const body = await this.login({ user })
46
47 return body.access_token
48 } catch (err) {
49 throw new Error(`Cannot authenticate. Please check your username/password. (${err})`)
50 }
51 }
52
53 loginUsingExternalToken (options: OverrideCommandOptions & {
54 username: string
55 externalAuthToken: string
56 }) {
57 const { username, externalAuthToken } = options
58 const path = '/api/v1/users/token'
59
60 const body = {
61 client_id: this.server.store.client.id,
62 client_secret: this.server.store.client.secret,
63 username: username,
64 response_type: 'code',
65 grant_type: 'password',
66 scope: 'upload',
67 externalAuthToken
68 }
69
70 return this.postBodyRequest({
71 ...options,
72
73 path,
74 requestType: 'form',
75 fields: body,
76 implicitToken: false,
77 defaultExpectedStatus: HttpStatusCode.OK_200
78 })
79 }
80
81 logout (options: OverrideCommandOptions & {
82 token: string
83 }) {
84 const path = '/api/v1/users/revoke-token'
85
86 return unwrapBody<{ redirectUrl: string }>(this.postBodyRequest({
87 ...options,
88
89 path,
90 requestType: 'form',
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 }))
94 }
95
96 refreshToken (options: OverrideCommandOptions & {
97 refreshToken: string
98 }) {
99 const path = '/api/v1/users/token'
100
101 const body = {
102 client_id: this.server.store.client.id,
103 client_secret: this.server.store.client.secret,
104 refresh_token: options.refreshToken,
105 response_type: 'code',
106 grant_type: 'refresh_token'
107 }
108
109 return this.postBodyRequest({
110 ...options,
111
112 path,
113 requestType: 'form',
114 fields: body,
115 implicitToken: false,
116 defaultExpectedStatus: HttpStatusCode.OK_200
117 })
118 }
119
120 getClient (options: OverrideCommandOptions = {}) {
121 const path = '/api/v1/oauth-clients/local'
122
123 return this.getRequestBody<{ client_id: string, client_secret: string }>({
124 ...options,
125
126 path,
127 host: this.server.host,
128 implicitToken: false,
129 defaultExpectedStatus: HttpStatusCode.OK_200
130 })
131 }
132}
diff --git a/shared/server-commands/users/login.ts b/shared/server-commands/users/login.ts
new file mode 100644
index 000000000..f1df027d3
--- /dev/null
+++ b/shared/server-commands/users/login.ts
@@ -0,0 +1,19 @@
1import { PeerTubeServer } from '../server/server'
2
3function setAccessTokensToServers (servers: PeerTubeServer[]) {
4 const tasks: Promise<any>[] = []
5
6 for (const server of servers) {
7 const p = server.login.getAccessToken()
8 .then(t => { server.accessToken = t })
9 tasks.push(p)
10 }
11
12 return Promise.all(tasks)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 setAccessTokensToServers
19}
diff --git a/shared/server-commands/users/notifications-command.ts b/shared/server-commands/users/notifications-command.ts
new file mode 100644
index 000000000..692420b8b
--- /dev/null
+++ b/shared/server-commands/users/notifications-command.ts
@@ -0,0 +1,86 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { UserNotification, UserNotificationSetting } from '../../models/users'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class NotificationsCommand extends AbstractCommand {
6
7 updateMySettings (options: OverrideCommandOptions & {
8 settings: UserNotificationSetting
9 }) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return this.putBodyRequest({
13 ...options,
14
15 path,
16 fields: options.settings,
17 implicitToken: true,
18 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
19 })
20 }
21
22 list (options: OverrideCommandOptions & {
23 start?: number
24 count?: number
25 unread?: boolean
26 sort?: string
27 }) {
28 const { start, count, unread, sort = '-createdAt' } = options
29 const path = '/api/v1/users/me/notifications'
30
31 return this.getRequestBody<ResultList<UserNotification>>({
32 ...options,
33
34 path,
35 query: {
36 start,
37 count,
38 sort,
39 unread
40 },
41 implicitToken: true,
42 defaultExpectedStatus: HttpStatusCode.OK_200
43 })
44 }
45
46 markAsRead (options: OverrideCommandOptions & {
47 ids: number[]
48 }) {
49 const { ids } = options
50 const path = '/api/v1/users/me/notifications/read'
51
52 return this.postBodyRequest({
53 ...options,
54
55 path,
56 fields: { ids },
57 implicitToken: true,
58 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
59 })
60 }
61
62 markAsReadAll (options: OverrideCommandOptions) {
63 const path = '/api/v1/users/me/notifications/read-all'
64
65 return this.postBodyRequest({
66 ...options,
67
68 path,
69 implicitToken: true,
70 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
71 })
72 }
73
74 async getLatest (options: OverrideCommandOptions = {}) {
75 const { total, data } = await this.list({
76 ...options,
77 start: 0,
78 count: 1,
79 sort: '-createdAt'
80 })
81
82 if (total === 0) return undefined
83
84 return data[0]
85 }
86}
diff --git a/shared/server-commands/users/notifications.ts b/shared/server-commands/users/notifications.ts
new file mode 100644
index 000000000..07ccb0f8d
--- /dev/null
+++ b/shared/server-commands/users/notifications.ts
@@ -0,0 +1,795 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { inspect } from 'util'
5import { AbuseState, PluginType } from '@shared/models'
6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
7import { MockSmtpServer } from '../mock-servers/mock-email'
8import { PeerTubeServer } from '../server'
9import { doubleFollow } from '../server/follows'
10import { createMultipleServers } from '../server/servers'
11import { setAccessTokensToServers } from './login'
12
13type CheckerBaseParams = {
14 server: PeerTubeServer
15 emails: any[]
16 socketNotifications: UserNotification[]
17 token: string
18 check?: { web: boolean, mail: boolean }
19}
20
21type CheckerType = 'presence' | 'absence'
22
23function getAllNotificationsSettings (): UserNotificationSetting {
24 return {
25 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
26 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
27 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
28 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
29 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
30 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
31 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
32 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
33 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
34 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
35 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
36 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
37 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
38 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
39 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
40 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
41 }
42}
43
44async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
45 videoName: string
46 shortUUID: string
47 checkType: CheckerType
48}) {
49 const { videoName, shortUUID } = options
50 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
51
52 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
53 if (checkType === 'presence') {
54 expect(notification).to.not.be.undefined
55 expect(notification.type).to.equal(notificationType)
56
57 checkVideo(notification.video, videoName, shortUUID)
58 checkActor(notification.video.channel)
59 } else {
60 expect(notification).to.satisfy((n: UserNotification) => {
61 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
62 })
63 }
64 }
65
66 function emailNotificationFinder (email: object) {
67 const text = email['text']
68 return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
69 }
70
71 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
72}
73
74async function checkVideoIsPublished (options: CheckerBaseParams & {
75 videoName: string
76 shortUUID: string
77 checkType: CheckerType
78}) {
79 const { videoName, shortUUID } = options
80 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
81
82 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
83 if (checkType === 'presence') {
84 expect(notification).to.not.be.undefined
85 expect(notification.type).to.equal(notificationType)
86
87 checkVideo(notification.video, videoName, shortUUID)
88 checkActor(notification.video.channel)
89 } else {
90 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
91 }
92 }
93
94 function emailNotificationFinder (email: object) {
95 const text: string = email['text']
96 return text.includes(shortUUID) && text.includes('Your video')
97 }
98
99 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
100}
101
102async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
103 videoName: string
104 shortUUID: string
105 url: string
106 success: boolean
107 checkType: CheckerType
108}) {
109 const { videoName, shortUUID, url, success } = options
110
111 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
112
113 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
114 if (checkType === 'presence') {
115 expect(notification).to.not.be.undefined
116 expect(notification.type).to.equal(notificationType)
117
118 expect(notification.videoImport.targetUrl).to.equal(url)
119
120 if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
121 } else {
122 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
123 }
124 }
125
126 function emailNotificationFinder (email: object) {
127 const text: string = email['text']
128 const toFind = success ? ' finished' : ' error'
129
130 return text.includes(url) && text.includes(toFind)
131 }
132
133 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
134}
135
136async function checkUserRegistered (options: CheckerBaseParams & {
137 username: string
138 checkType: CheckerType
139}) {
140 const { username } = options
141 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
142
143 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
144 if (checkType === 'presence') {
145 expect(notification).to.not.be.undefined
146 expect(notification.type).to.equal(notificationType)
147
148 checkActor(notification.account)
149 expect(notification.account.name).to.equal(username)
150 } else {
151 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
152 }
153 }
154
155 function emailNotificationFinder (email: object) {
156 const text: string = email['text']
157
158 return text.includes(' registered.') && text.includes(username)
159 }
160
161 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
162}
163
164async function checkNewActorFollow (options: CheckerBaseParams & {
165 followType: 'channel' | 'account'
166 followerName: string
167 followerDisplayName: string
168 followingDisplayName: string
169 checkType: CheckerType
170}) {
171 const { followType, followerName, followerDisplayName, followingDisplayName } = options
172 const notificationType = UserNotificationType.NEW_FOLLOW
173
174 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
175 if (checkType === 'presence') {
176 expect(notification).to.not.be.undefined
177 expect(notification.type).to.equal(notificationType)
178
179 checkActor(notification.actorFollow.follower)
180 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
181 expect(notification.actorFollow.follower.name).to.equal(followerName)
182 expect(notification.actorFollow.follower.host).to.not.be.undefined
183
184 const following = notification.actorFollow.following
185 expect(following.displayName).to.equal(followingDisplayName)
186 expect(following.type).to.equal(followType)
187 } else {
188 expect(notification).to.satisfy(n => {
189 return n.type !== notificationType ||
190 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
191 })
192 }
193 }
194
195 function emailNotificationFinder (email: object) {
196 const text: string = email['text']
197
198 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
199 }
200
201 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
202}
203
204async function checkNewInstanceFollower (options: CheckerBaseParams & {
205 followerHost: string
206 checkType: CheckerType
207}) {
208 const { followerHost } = options
209 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
210
211 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
212 if (checkType === 'presence') {
213 expect(notification).to.not.be.undefined
214 expect(notification.type).to.equal(notificationType)
215
216 checkActor(notification.actorFollow.follower)
217 expect(notification.actorFollow.follower.name).to.equal('peertube')
218 expect(notification.actorFollow.follower.host).to.equal(followerHost)
219
220 expect(notification.actorFollow.following.name).to.equal('peertube')
221 } else {
222 expect(notification).to.satisfy(n => {
223 return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
224 })
225 }
226 }
227
228 function emailNotificationFinder (email: object) {
229 const text: string = email['text']
230
231 return text.includes('instance has a new follower') && text.includes(followerHost)
232 }
233
234 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
235}
236
237async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
238 followerHost: string
239 followingHost: string
240 checkType: CheckerType
241}) {
242 const { followerHost, followingHost } = options
243 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
244
245 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
246 if (checkType === 'presence') {
247 expect(notification).to.not.be.undefined
248 expect(notification.type).to.equal(notificationType)
249
250 const following = notification.actorFollow.following
251 checkActor(following)
252 expect(following.name).to.equal('peertube')
253 expect(following.host).to.equal(followingHost)
254
255 expect(notification.actorFollow.follower.name).to.equal('peertube')
256 expect(notification.actorFollow.follower.host).to.equal(followerHost)
257 } else {
258 expect(notification).to.satisfy(n => {
259 return n.type !== notificationType || n.actorFollow.following.host !== followingHost
260 })
261 }
262 }
263
264 function emailNotificationFinder (email: object) {
265 const text: string = email['text']
266
267 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
268 }
269
270 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
271}
272
273async function checkCommentMention (options: CheckerBaseParams & {
274 shortUUID: string
275 commentId: number
276 threadId: number
277 byAccountDisplayName: string
278 checkType: CheckerType
279}) {
280 const { shortUUID, commentId, threadId, byAccountDisplayName } = options
281 const notificationType = UserNotificationType.COMMENT_MENTION
282
283 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
284 if (checkType === 'presence') {
285 expect(notification).to.not.be.undefined
286 expect(notification.type).to.equal(notificationType)
287
288 checkComment(notification.comment, commentId, threadId)
289 checkActor(notification.comment.account)
290 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
291
292 checkVideo(notification.comment.video, undefined, shortUUID)
293 } else {
294 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
295 }
296 }
297
298 function emailNotificationFinder (email: object) {
299 const text: string = email['text']
300
301 return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
302 }
303
304 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
305}
306
307let lastEmailCount = 0
308
309async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
310 shortUUID: string
311 commentId: number
312 threadId: number
313 checkType: CheckerType
314}) {
315 const { server, shortUUID, commentId, threadId, checkType, emails } = options
316 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
317
318 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
319 if (checkType === 'presence') {
320 expect(notification).to.not.be.undefined
321 expect(notification.type).to.equal(notificationType)
322
323 checkComment(notification.comment, commentId, threadId)
324 checkActor(notification.comment.account)
325 checkVideo(notification.comment.video, undefined, shortUUID)
326 } else {
327 expect(notification).to.satisfy((n: UserNotification) => {
328 return n === undefined || n.comment === undefined || n.comment.id !== commentId
329 })
330 }
331 }
332
333 const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}`
334
335 function emailNotificationFinder (email: object) {
336 return email['text'].indexOf(commentUrl) !== -1
337 }
338
339 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
340
341 if (checkType === 'presence') {
342 // We cannot detect email duplicates, so check we received another email
343 expect(emails).to.have.length.above(lastEmailCount)
344 lastEmailCount = emails.length
345 }
346}
347
348async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
349 shortUUID: string
350 videoName: string
351 checkType: CheckerType
352}) {
353 const { shortUUID, videoName } = options
354 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
355
356 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
357 if (checkType === 'presence') {
358 expect(notification).to.not.be.undefined
359 expect(notification.type).to.equal(notificationType)
360
361 expect(notification.abuse.id).to.be.a('number')
362 checkVideo(notification.abuse.video, videoName, shortUUID)
363 } else {
364 expect(notification).to.satisfy((n: UserNotification) => {
365 return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
366 })
367 }
368 }
369
370 function emailNotificationFinder (email: object) {
371 const text = email['text']
372 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
373 }
374
375 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
376}
377
378async function checkNewAbuseMessage (options: CheckerBaseParams & {
379 abuseId: number
380 message: string
381 toEmail: string
382 checkType: CheckerType
383}) {
384 const { abuseId, message, toEmail } = options
385 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
386
387 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
388 if (checkType === 'presence') {
389 expect(notification).to.not.be.undefined
390 expect(notification.type).to.equal(notificationType)
391
392 expect(notification.abuse.id).to.equal(abuseId)
393 } else {
394 expect(notification).to.satisfy((n: UserNotification) => {
395 return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
396 })
397 }
398 }
399
400 function emailNotificationFinder (email: object) {
401 const text = email['text']
402 const to = email['to'].filter(t => t.address === toEmail)
403
404 return text.indexOf(message) !== -1 && to.length !== 0
405 }
406
407 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
408}
409
410async function checkAbuseStateChange (options: CheckerBaseParams & {
411 abuseId: number
412 state: AbuseState
413 checkType: CheckerType
414}) {
415 const { abuseId, state } = options
416 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
417
418 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
419 if (checkType === 'presence') {
420 expect(notification).to.not.be.undefined
421 expect(notification.type).to.equal(notificationType)
422
423 expect(notification.abuse.id).to.equal(abuseId)
424 expect(notification.abuse.state).to.equal(state)
425 } else {
426 expect(notification).to.satisfy((n: UserNotification) => {
427 return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId
428 })
429 }
430 }
431
432 function emailNotificationFinder (email: object) {
433 const text = email['text']
434
435 const contains = state === AbuseState.ACCEPTED
436 ? ' accepted'
437 : ' rejected'
438
439 return text.indexOf(contains) !== -1
440 }
441
442 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
443}
444
445async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
446 shortUUID: string
447 videoName: string
448 checkType: CheckerType
449}) {
450 const { shortUUID, videoName } = options
451 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
452
453 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
454 if (checkType === 'presence') {
455 expect(notification).to.not.be.undefined
456 expect(notification.type).to.equal(notificationType)
457
458 expect(notification.abuse.id).to.be.a('number')
459 checkVideo(notification.abuse.comment.video, videoName, shortUUID)
460 } else {
461 expect(notification).to.satisfy((n: UserNotification) => {
462 return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
463 })
464 }
465 }
466
467 function emailNotificationFinder (email: object) {
468 const text = email['text']
469 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
470 }
471
472 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
473}
474
475async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
476 displayName: string
477 checkType: CheckerType
478}) {
479 const { displayName } = options
480 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
481
482 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
483 if (checkType === 'presence') {
484 expect(notification).to.not.be.undefined
485 expect(notification.type).to.equal(notificationType)
486
487 expect(notification.abuse.id).to.be.a('number')
488 expect(notification.abuse.account.displayName).to.equal(displayName)
489 } else {
490 expect(notification).to.satisfy((n: UserNotification) => {
491 return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName
492 })
493 }
494 }
495
496 function emailNotificationFinder (email: object) {
497 const text = email['text']
498 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
499 }
500
501 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
502}
503
504async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
505 shortUUID: string
506 videoName: string
507 checkType: CheckerType
508}) {
509 const { shortUUID, videoName } = options
510 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
511
512 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
513 if (checkType === 'presence') {
514 expect(notification).to.not.be.undefined
515 expect(notification.type).to.equal(notificationType)
516
517 expect(notification.videoBlacklist.video.id).to.be.a('number')
518 checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
519 } else {
520 expect(notification).to.satisfy((n: UserNotification) => {
521 return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
522 })
523 }
524 }
525
526 function emailNotificationFinder (email: object) {
527 const text = email['text']
528 return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
529 }
530
531 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
532}
533
534async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
535 shortUUID: string
536 videoName: string
537 blacklistType: 'blacklist' | 'unblacklist'
538}) {
539 const { videoName, shortUUID, blacklistType } = options
540 const notificationType = blacklistType === 'blacklist'
541 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
542 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
543
544 function notificationChecker (notification: UserNotification) {
545 expect(notification).to.not.be.undefined
546 expect(notification.type).to.equal(notificationType)
547
548 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
549
550 checkVideo(video, videoName, shortUUID)
551 }
552
553 function emailNotificationFinder (email: object) {
554 const text = email['text']
555 const blacklistText = blacklistType === 'blacklist'
556 ? 'blacklisted'
557 : 'unblacklisted'
558
559 return text.includes(shortUUID) && text.includes(blacklistText)
560 }
561
562 await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
563}
564
565async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
566 latestVersion: string
567 checkType: CheckerType
568}) {
569 const { latestVersion } = options
570 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
571
572 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
573 if (checkType === 'presence') {
574 expect(notification).to.not.be.undefined
575 expect(notification.type).to.equal(notificationType)
576
577 expect(notification.peertube).to.exist
578 expect(notification.peertube.latestVersion).to.equal(latestVersion)
579 } else {
580 expect(notification).to.satisfy((n: UserNotification) => {
581 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
582 })
583 }
584 }
585
586 function emailNotificationFinder (email: object) {
587 const text = email['text']
588
589 return text.includes(latestVersion)
590 }
591
592 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
593}
594
595async function checkNewPluginVersion (options: CheckerBaseParams & {
596 pluginType: PluginType
597 pluginName: string
598 checkType: CheckerType
599}) {
600 const { pluginName, pluginType } = options
601 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
602
603 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
604 if (checkType === 'presence') {
605 expect(notification).to.not.be.undefined
606 expect(notification.type).to.equal(notificationType)
607
608 expect(notification.plugin.name).to.equal(pluginName)
609 expect(notification.plugin.type).to.equal(pluginType)
610 } else {
611 expect(notification).to.satisfy((n: UserNotification) => {
612 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
613 })
614 }
615 }
616
617 function emailNotificationFinder (email: object) {
618 const text = email['text']
619
620 return text.includes(pluginName)
621 }
622
623 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
624}
625
626async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
627 const userNotifications: UserNotification[] = []
628 const adminNotifications: UserNotification[] = []
629 const adminNotificationsServer2: UserNotification[] = []
630 const emails: object[] = []
631
632 const port = await MockSmtpServer.Instance.collectEmails(emails)
633
634 const overrideConfig = {
635 smtp: {
636 hostname: 'localhost',
637 port
638 },
639 signup: {
640 limit: 20
641 }
642 }
643 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
644
645 await setAccessTokensToServers(servers)
646
647 if (serversCount > 1) {
648 await doubleFollow(servers[0], servers[1])
649 }
650
651 const user = { username: 'user_1', password: 'super password' }
652 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
653 const userAccessToken = await servers[0].login.getAccessToken(user)
654
655 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
656 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
657
658 if (serversCount > 1) {
659 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
660 }
661
662 {
663 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
664 socket.on('new-notification', n => userNotifications.push(n))
665 }
666 {
667 const socket = servers[0].socketIO.getUserNotificationSocket()
668 socket.on('new-notification', n => adminNotifications.push(n))
669 }
670
671 if (serversCount > 1) {
672 const socket = servers[1].socketIO.getUserNotificationSocket()
673 socket.on('new-notification', n => adminNotificationsServer2.push(n))
674 }
675
676 const { videoChannels } = await servers[0].users.getMyInfo()
677 const channelId = videoChannels[0].id
678
679 return {
680 userNotifications,
681 adminNotifications,
682 adminNotificationsServer2,
683 userAccessToken,
684 emails,
685 servers,
686 channelId
687 }
688}
689
690// ---------------------------------------------------------------------------
691
692export {
693 getAllNotificationsSettings,
694
695 CheckerBaseParams,
696 CheckerType,
697 checkMyVideoImportIsFinished,
698 checkUserRegistered,
699 checkAutoInstanceFollowing,
700 checkVideoIsPublished,
701 checkNewVideoFromSubscription,
702 checkNewActorFollow,
703 checkNewCommentOnMyVideo,
704 checkNewBlacklistOnMyVideo,
705 checkCommentMention,
706 checkNewVideoAbuseForModerators,
707 checkVideoAutoBlacklistForModerators,
708 checkNewAbuseMessage,
709 checkAbuseStateChange,
710 checkNewInstanceFollower,
711 prepareNotificationsTest,
712 checkNewCommentAbuseForModerators,
713 checkNewAccountAbuseForModerators,
714 checkNewPeerTubeVersion,
715 checkNewPluginVersion
716}
717
718// ---------------------------------------------------------------------------
719
720async function checkNotification (options: CheckerBaseParams & {
721 notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
722 emailNotificationFinder: (email: object) => boolean
723 checkType: CheckerType
724}) {
725 const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
726
727 const check = options.check || { web: true, mail: true }
728
729 if (check.web) {
730 const notification = await server.notifications.getLatest({ token: token })
731
732 if (notification || checkType !== 'absence') {
733 notificationChecker(notification, checkType)
734 }
735
736 const socketNotification = socketNotifications.find(n => {
737 try {
738 notificationChecker(n, 'presence')
739 return true
740 } catch {
741 return false
742 }
743 })
744
745 if (checkType === 'presence') {
746 const obj = inspect(socketNotifications, { depth: 5 })
747 expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
748 } else {
749 const obj = inspect(socketNotification, { depth: 5 })
750 expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
751 }
752 }
753
754 if (check.mail) {
755 // Last email
756 const email = emails
757 .slice()
758 .reverse()
759 .find(e => emailNotificationFinder(e))
760
761 if (checkType === 'presence') {
762 const texts = emails.map(e => e.text)
763 expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
764 } else {
765 expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
766 }
767 }
768}
769
770function checkVideo (video: any, videoName?: string, shortUUID?: string) {
771 if (videoName) {
772 expect(video.name).to.be.a('string')
773 expect(video.name).to.not.be.empty
774 expect(video.name).to.equal(videoName)
775 }
776
777 if (shortUUID) {
778 expect(video.shortUUID).to.be.a('string')
779 expect(video.shortUUID).to.not.be.empty
780 expect(video.shortUUID).to.equal(shortUUID)
781 }
782
783 expect(video.id).to.be.a('number')
784}
785
786function checkActor (actor: any) {
787 expect(actor.displayName).to.be.a('string')
788 expect(actor.displayName).to.not.be.empty
789 expect(actor.host).to.not.be.undefined
790}
791
792function checkComment (comment: any, commentId: number, threadId: number) {
793 expect(comment.id).to.equal(commentId)
794 expect(comment.threadId).to.equal(threadId)
795}
diff --git a/shared/server-commands/users/subscriptions-command.ts b/shared/server-commands/users/subscriptions-command.ts
new file mode 100644
index 000000000..edc60e612
--- /dev/null
+++ b/shared/server-commands/users/subscriptions-command.ts
@@ -0,0 +1,99 @@
1import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class SubscriptionsCommand extends AbstractCommand {
5
6 add (options: OverrideCommandOptions & {
7 targetUri: string
8 }) {
9 const path = '/api/v1/users/me/subscriptions'
10
11 return this.postBodyRequest({
12 ...options,
13
14 path,
15 fields: { uri: options.targetUri },
16 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
18 })
19 }
20
21 list (options: OverrideCommandOptions & {
22 sort?: string // default -createdAt
23 search?: string
24 } = {}) {
25 const { sort = '-createdAt', search } = options
26 const path = '/api/v1/users/me/subscriptions'
27
28 return this.getRequestBody<ResultList<VideoChannel>>({
29 ...options,
30
31 path,
32 query: {
33 sort,
34 search
35 },
36 implicitToken: true,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 listVideos (options: OverrideCommandOptions & {
42 sort?: string // default -createdAt
43 } = {}) {
44 const { sort = '-createdAt' } = options
45 const path = '/api/v1/users/me/subscriptions/videos'
46
47 return this.getRequestBody<ResultList<Video>>({
48 ...options,
49
50 path,
51 query: { sort },
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 get (options: OverrideCommandOptions & {
58 uri: string
59 }) {
60 const path = '/api/v1/users/me/subscriptions/' + options.uri
61
62 return this.getRequestBody<VideoChannel>({
63 ...options,
64
65 path,
66 implicitToken: true,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 remove (options: OverrideCommandOptions & {
72 uri: string
73 }) {
74 const path = '/api/v1/users/me/subscriptions/' + options.uri
75
76 return this.deleteRequest({
77 ...options,
78
79 path,
80 implicitToken: true,
81 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
82 })
83 }
84
85 exist (options: OverrideCommandOptions & {
86 uris: string[]
87 }) {
88 const path = '/api/v1/users/me/subscriptions/exist'
89
90 return this.getRequestBody<{ [id: string ]: boolean }>({
91 ...options,
92
93 path,
94 query: { 'uris[]': options.uris },
95 implicitToken: true,
96 defaultExpectedStatus: HttpStatusCode.OK_200
97 })
98 }
99}
diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts
new file mode 100644
index 000000000..90c5f2183
--- /dev/null
+++ b/shared/server-commands/users/users-command.ts
@@ -0,0 +1,416 @@
1import { omit } from 'lodash'
2import { pick } from '@shared/core-utils'
3import {
4 HttpStatusCode,
5 MyUser,
6 ResultList,
7 User,
8 UserAdminFlag,
9 UserCreateResult,
10 UserRole,
11 UserUpdate,
12 UserUpdateMe,
13 UserVideoQuota,
14 UserVideoRate
15} from '@shared/models'
16import { ScopedToken } from '@shared/models/users/user-scoped-token'
17import { unwrapBody } from '../requests'
18import { AbstractCommand, OverrideCommandOptions } from '../shared'
19
20export class UsersCommand extends AbstractCommand {
21
22 askResetPassword (options: OverrideCommandOptions & {
23 email: string
24 }) {
25 const { email } = options
26 const path = '/api/v1/users/ask-reset-password'
27
28 return this.postBodyRequest({
29 ...options,
30
31 path,
32 fields: { email },
33 implicitToken: false,
34 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
35 })
36 }
37
38 resetPassword (options: OverrideCommandOptions & {
39 userId: number
40 verificationString: string
41 password: string
42 }) {
43 const { userId, verificationString, password } = options
44 const path = '/api/v1/users/' + userId + '/reset-password'
45
46 return this.postBodyRequest({
47 ...options,
48
49 path,
50 fields: { password, verificationString },
51 implicitToken: false,
52 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
53 })
54 }
55
56 // ---------------------------------------------------------------------------
57
58 askSendVerifyEmail (options: OverrideCommandOptions & {
59 email: string
60 }) {
61 const { email } = options
62 const path = '/api/v1/users/ask-send-verify-email'
63
64 return this.postBodyRequest({
65 ...options,
66
67 path,
68 fields: { email },
69 implicitToken: false,
70 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
71 })
72 }
73
74 verifyEmail (options: OverrideCommandOptions & {
75 userId: number
76 verificationString: string
77 isPendingEmail?: boolean // default false
78 }) {
79 const { userId, verificationString, isPendingEmail = false } = options
80 const path = '/api/v1/users/' + userId + '/verify-email'
81
82 return this.postBodyRequest({
83 ...options,
84
85 path,
86 fields: {
87 verificationString,
88 isPendingEmail
89 },
90 implicitToken: false,
91 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
92 })
93 }
94
95 // ---------------------------------------------------------------------------
96
97 banUser (options: OverrideCommandOptions & {
98 userId: number
99 reason?: string
100 }) {
101 const { userId, reason } = options
102 const path = '/api/v1/users' + '/' + userId + '/block'
103
104 return this.postBodyRequest({
105 ...options,
106
107 path,
108 fields: { reason },
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 unbanUser (options: OverrideCommandOptions & {
115 userId: number
116 }) {
117 const { userId } = options
118 const path = '/api/v1/users' + '/' + userId + '/unblock'
119
120 return this.postBodyRequest({
121 ...options,
122
123 path,
124 implicitToken: true,
125 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
126 })
127 }
128
129 // ---------------------------------------------------------------------------
130
131 getMyScopedTokens (options: OverrideCommandOptions = {}) {
132 const path = '/api/v1/users/scoped-tokens'
133
134 return this.getRequestBody<ScopedToken>({
135 ...options,
136
137 path,
138 implicitToken: true,
139 defaultExpectedStatus: HttpStatusCode.OK_200
140 })
141 }
142
143 renewMyScopedTokens (options: OverrideCommandOptions = {}) {
144 const path = '/api/v1/users/scoped-tokens'
145
146 return this.postBodyRequest({
147 ...options,
148
149 path,
150 implicitToken: true,
151 defaultExpectedStatus: HttpStatusCode.OK_200
152 })
153 }
154
155 // ---------------------------------------------------------------------------
156
157 create (options: OverrideCommandOptions & {
158 username: string
159 password?: string
160 videoQuota?: number
161 videoQuotaDaily?: number
162 role?: UserRole
163 adminFlags?: UserAdminFlag
164 }) {
165 const {
166 username,
167 adminFlags,
168 password = 'password',
169 videoQuota = 42000000,
170 videoQuotaDaily = -1,
171 role = UserRole.USER
172 } = options
173
174 const path = '/api/v1/users'
175
176 return unwrapBody<{ user: UserCreateResult }>(this.postBodyRequest({
177 ...options,
178
179 path,
180 fields: {
181 username,
182 password,
183 role,
184 adminFlags,
185 email: username + '@example.com',
186 videoQuota,
187 videoQuotaDaily
188 },
189 implicitToken: true,
190 defaultExpectedStatus: HttpStatusCode.OK_200
191 })).then(res => res.user)
192 }
193
194 async generate (username: string, role?: UserRole) {
195 const password = 'password'
196 const user = await this.create({ username, password, role })
197
198 const token = await this.server.login.getAccessToken({ username, password })
199
200 const me = await this.getMyInfo({ token })
201
202 return {
203 token,
204 userId: user.id,
205 userChannelId: me.videoChannels[0].id,
206 userChannelName: me.videoChannels[0].name
207 }
208 }
209
210 async generateUserAndToken (username: string, role?: UserRole) {
211 const password = 'password'
212 await this.create({ username, password, role })
213
214 return this.server.login.getAccessToken({ username, password })
215 }
216
217 register (options: OverrideCommandOptions & {
218 username: string
219 password?: string
220 displayName?: string
221 channel?: {
222 name: string
223 displayName: string
224 }
225 }) {
226 const { username, password = 'password', displayName, channel } = options
227 const path = '/api/v1/users/register'
228
229 return this.postBodyRequest({
230 ...options,
231
232 path,
233 fields: {
234 username,
235 password,
236 email: username + '@example.com',
237 displayName,
238 channel
239 },
240 implicitToken: false,
241 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
242 })
243 }
244
245 // ---------------------------------------------------------------------------
246
247 getMyInfo (options: OverrideCommandOptions = {}) {
248 const path = '/api/v1/users/me'
249
250 return this.getRequestBody<MyUser>({
251 ...options,
252
253 path,
254 implicitToken: true,
255 defaultExpectedStatus: HttpStatusCode.OK_200
256 })
257 }
258
259 getMyQuotaUsed (options: OverrideCommandOptions = {}) {
260 const path = '/api/v1/users/me/video-quota-used'
261
262 return this.getRequestBody<UserVideoQuota>({
263 ...options,
264
265 path,
266 implicitToken: true,
267 defaultExpectedStatus: HttpStatusCode.OK_200
268 })
269 }
270
271 getMyRating (options: OverrideCommandOptions & {
272 videoId: number | string
273 }) {
274 const { videoId } = options
275 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
276
277 return this.getRequestBody<UserVideoRate>({
278 ...options,
279
280 path,
281 implicitToken: true,
282 defaultExpectedStatus: HttpStatusCode.OK_200
283 })
284 }
285
286 deleteMe (options: OverrideCommandOptions = {}) {
287 const path = '/api/v1/users/me'
288
289 return this.deleteRequest({
290 ...options,
291
292 path,
293 implicitToken: true,
294 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
295 })
296 }
297
298 updateMe (options: OverrideCommandOptions & UserUpdateMe) {
299 const path = '/api/v1/users/me'
300
301 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
302
303 return this.putBodyRequest({
304 ...options,
305
306 path,
307 fields: toSend,
308 implicitToken: true,
309 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
310 })
311 }
312
313 updateMyAvatar (options: OverrideCommandOptions & {
314 fixture: string
315 }) {
316 const { fixture } = options
317 const path = '/api/v1/users/me/avatar/pick'
318
319 return this.updateImageRequest({
320 ...options,
321
322 path,
323 fixture,
324 fieldname: 'avatarfile',
325
326 implicitToken: true,
327 defaultExpectedStatus: HttpStatusCode.OK_200
328 })
329 }
330
331 // ---------------------------------------------------------------------------
332
333 get (options: OverrideCommandOptions & {
334 userId: number
335 withStats?: boolean // default false
336 }) {
337 const { userId, withStats } = options
338 const path = '/api/v1/users/' + userId
339
340 return this.getRequestBody<User>({
341 ...options,
342
343 path,
344 query: { withStats },
345 implicitToken: true,
346 defaultExpectedStatus: HttpStatusCode.OK_200
347 })
348 }
349
350 list (options: OverrideCommandOptions & {
351 start?: number
352 count?: number
353 sort?: string
354 search?: string
355 blocked?: boolean
356 } = {}) {
357 const path = '/api/v1/users'
358
359 return this.getRequestBody<ResultList<User>>({
360 ...options,
361
362 path,
363 query: pick(options, [ 'start', 'count', 'sort', 'search', 'blocked' ]),
364 implicitToken: true,
365 defaultExpectedStatus: HttpStatusCode.OK_200
366 })
367 }
368
369 remove (options: OverrideCommandOptions & {
370 userId: number
371 }) {
372 const { userId } = options
373 const path = '/api/v1/users/' + userId
374
375 return this.deleteRequest({
376 ...options,
377
378 path,
379 implicitToken: true,
380 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
381 })
382 }
383
384 update (options: OverrideCommandOptions & {
385 userId: number
386 email?: string
387 emailVerified?: boolean
388 videoQuota?: number
389 videoQuotaDaily?: number
390 password?: string
391 adminFlags?: UserAdminFlag
392 pluginAuth?: string
393 role?: UserRole
394 }) {
395 const path = '/api/v1/users/' + options.userId
396
397 const toSend: UserUpdate = {}
398 if (options.password !== undefined && options.password !== null) toSend.password = options.password
399 if (options.email !== undefined && options.email !== null) toSend.email = options.email
400 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend.emailVerified = options.emailVerified
401 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend.videoQuota = options.videoQuota
402 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend.videoQuotaDaily = options.videoQuotaDaily
403 if (options.role !== undefined && options.role !== null) toSend.role = options.role
404 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend.adminFlags = options.adminFlags
405 if (options.pluginAuth !== undefined) toSend.pluginAuth = options.pluginAuth
406
407 return this.putBodyRequest({
408 ...options,
409
410 path,
411 fields: toSend,
412 implicitToken: true,
413 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
414 })
415 }
416}