aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/models/activitypub/activity.ts31
-rw-r--r--shared/models/activitypub/activitypub-ordered-collection.ts5
-rw-r--r--shared/models/activitypub/objects/cache-file-object.ts4
-rw-r--r--shared/models/activitypub/objects/common-objects.ts60
-rw-r--r--shared/models/activitypub/objects/dislike-object.ts3
-rw-r--r--shared/models/activitypub/objects/object.model.ts1
-rw-r--r--shared/models/actors/actor.model.ts2
-rw-r--r--shared/models/blocklist/account-block.model.ts7
-rw-r--r--shared/models/blocklist/index.ts2
-rw-r--r--shared/models/blocklist/server-block.model.ts9
-rw-r--r--shared/models/i18n/i18n.ts9
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/search/videos-search-query.model.ts3
-rw-r--r--shared/models/server/contact-form.model.ts5
-rw-r--r--shared/models/server/custom-config.model.ts8
-rw-r--r--shared/models/server/index.ts6
-rw-r--r--shared/models/server/job.model.ts3
-rw-r--r--shared/models/server/server-config.model.ts22
-rw-r--r--shared/models/server/server-stats.model.ts1
-rw-r--r--shared/models/users/index.ts2
-rw-r--r--shared/models/users/user-notification-setting.model.ts17
-rw-r--r--shared/models/users/user-notification.model.ts83
-rw-r--r--shared/models/users/user-right.enum.ts9
-rw-r--r--shared/models/users/user-role.ts6
-rw-r--r--shared/models/users/user-update-me.model.ts4
-rw-r--r--shared/models/users/user-update.model.ts2
-rw-r--r--shared/models/users/user.model.ts7
-rw-r--r--shared/models/videos/blacklist/video-blacklist-create.model.ts1
-rw-r--r--shared/models/videos/blacklist/video-blacklist.model.ts1
-rw-r--r--shared/models/videos/index.ts1
-rw-r--r--shared/models/videos/video-query.type.ts2
-rw-r--r--shared/models/videos/video-rate.type.ts2
-rw-r--r--shared/models/videos/video-resolution.enum.ts68
-rw-r--r--shared/models/videos/video-streaming-playlist.model.ts12
-rw-r--r--shared/models/videos/video-streaming-playlist.type.ts3
-rw-r--r--shared/models/videos/video-transcoding-fps.model.ts6
-rw-r--r--shared/models/videos/video.model.ts9
-rw-r--r--shared/utils/cli/cli.ts24
-rw-r--r--shared/utils/feeds/feeds.ts32
-rw-r--r--shared/utils/index.ts25
-rw-r--r--shared/utils/miscs/email-child-process.js27
-rw-r--r--shared/utils/miscs/email.ts64
-rw-r--r--shared/utils/miscs/miscs.ts101
-rw-r--r--shared/utils/miscs/sql.ts38
-rw-r--r--shared/utils/miscs/stubs.ts14
-rw-r--r--shared/utils/overviews/overviews.ts18
-rw-r--r--shared/utils/requests/activitypub.ts43
-rw-r--r--shared/utils/requests/check-api-params.ts40
-rw-r--r--shared/utils/requests/requests.ts177
-rw-r--r--shared/utils/search/video-channels.ts22
-rw-r--r--shared/utils/search/videos.ts77
-rw-r--r--shared/utils/server/activitypub.ts14
-rw-r--r--shared/utils/server/clients.ts19
-rw-r--r--shared/utils/server/config.ts142
-rw-r--r--shared/utils/server/contact-form.ts28
-rw-r--r--shared/utils/server/follows.ts79
-rw-r--r--shared/utils/server/jobs.ts82
-rw-r--r--shared/utils/server/redundancy.ts17
-rw-r--r--shared/utils/server/servers.ts215
-rw-r--r--shared/utils/server/stats.ts22
-rw-r--r--shared/utils/socket/socket-io.ts13
-rw-r--r--shared/utils/users/accounts.ts63
-rw-r--r--shared/utils/users/blocklist.ts197
-rw-r--r--shared/utils/users/login.ts62
-rw-r--r--shared/utils/users/user-notifications.ts437
-rw-r--r--shared/utils/users/user-subscriptions.ts82
-rw-r--r--shared/utils/users/users.ts304
-rw-r--r--shared/utils/videos/services.ts23
-rw-r--r--shared/utils/videos/video-abuses.ts65
-rw-r--r--shared/utils/videos/video-blacklist.ts74
-rw-r--r--shared/utils/videos/video-captions.ts71
-rw-r--r--shared/utils/videos/video-change-ownership.ts54
-rw-r--r--shared/utils/videos/video-channels.ts118
-rw-r--r--shared/utils/videos/video-comments.ts87
-rw-r--r--shared/utils/videos/video-history.ts39
-rw-r--r--shared/utils/videos/video-imports.ts57
-rw-r--r--shared/utils/videos/video-playlists.ts51
-rw-r--r--shared/utils/videos/videos.ts592
78 files changed, 4089 insertions, 37 deletions
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index 44cb99efb..89994f665 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -5,12 +5,14 @@ import { DislikeObject } from './objects/dislike-object'
5import { VideoAbuseObject } from './objects/video-abuse-object' 5import { VideoAbuseObject } from './objects/video-abuse-object'
6import { VideoCommentObject } from './objects/video-comment-object' 6import { VideoCommentObject } from './objects/video-comment-object'
7import { ViewObject } from './objects/view-object' 7import { ViewObject } from './objects/view-object'
8import { APObject } from './objects/object.model'
8 9
9export type Activity = ActivityCreate | ActivityUpdate | 10export type Activity = ActivityCreate | ActivityUpdate |
10 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | 11 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
11 ActivityUndo | ActivityLike | ActivityReject 12 ActivityUndo | ActivityLike | ActivityReject | ActivityView | ActivityDislike | ActivityFlag
12 13
13export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' 14export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' |
15 'View' | 'Dislike' | 'Flag'
14 16
15export interface ActivityAudience { 17export interface ActivityAudience {
16 to: string[] 18 to: string[]
@@ -59,15 +61,34 @@ export interface ActivityReject extends BaseActivity {
59 61
60export interface ActivityAnnounce extends BaseActivity { 62export interface ActivityAnnounce extends BaseActivity {
61 type: 'Announce' 63 type: 'Announce'
62 object: string | { id: string } 64 object: APObject
63} 65}
64 66
65export interface ActivityUndo extends BaseActivity { 67export interface ActivityUndo extends BaseActivity {
66 type: 'Undo', 68 type: 'Undo',
67 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce 69 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
68} 70}
69 71
70export interface ActivityLike extends BaseActivity { 72export interface ActivityLike extends BaseActivity {
71 type: 'Like', 73 type: 'Like',
72 object: string 74 object: APObject
75}
76
77export interface ActivityView extends BaseActivity {
78 type: 'View',
79 actor: string
80 object: APObject
81}
82
83export interface ActivityDislike extends BaseActivity {
84 id: string
85 type: 'Dislike'
86 actor: string
87 object: APObject
88}
89
90export interface ActivityFlag extends BaseActivity {
91 type: 'Flag',
92 content: string,
93 object: APObject
73} 94}
diff --git a/shared/models/activitypub/activitypub-ordered-collection.ts b/shared/models/activitypub/activitypub-ordered-collection.ts
index dfec0bb76..3de0890bb 100644
--- a/shared/models/activitypub/activitypub-ordered-collection.ts
+++ b/shared/models/activitypub/activitypub-ordered-collection.ts
@@ -2,6 +2,9 @@ export interface ActivityPubOrderedCollection<T> {
2 '@context': string[] 2 '@context': string[]
3 type: 'OrderedCollection' | 'OrderedCollectionPage' 3 type: 'OrderedCollection' | 'OrderedCollectionPage'
4 totalItems: number 4 totalItems: number
5 partOf?: string
6 orderedItems: T[] 5 orderedItems: T[]
6
7 partOf?: string
8 next?: string
9 first?: string
7} 10}
diff --git a/shared/models/activitypub/objects/cache-file-object.ts b/shared/models/activitypub/objects/cache-file-object.ts
index 0a5125f5b..4b0a3a724 100644
--- a/shared/models/activitypub/objects/cache-file-object.ts
+++ b/shared/models/activitypub/objects/cache-file-object.ts
@@ -1,9 +1,9 @@
1import { ActivityVideoUrlObject } from './common-objects' 1import { ActivityVideoUrlObject, ActivityPlaylistUrlObject } from './common-objects'
2 2
3export interface CacheFileObject { 3export interface CacheFileObject {
4 id: string 4 id: string
5 type: 'CacheFile', 5 type: 'CacheFile',
6 object: string 6 object: string
7 expires: string 7 expires: string
8 url: ActivityVideoUrlObject 8 url: ActivityVideoUrlObject | ActivityPlaylistUrlObject
9} 9}
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts
index 1de60da94..8c89810d6 100644
--- a/shared/models/activitypub/objects/common-objects.ts
+++ b/shared/models/activitypub/objects/common-objects.ts
@@ -19,28 +19,56 @@ export interface ActivityIconObject {
19 19
20export type ActivityVideoUrlObject = { 20export type ActivityVideoUrlObject = {
21 type: 'Link' 21 type: 'Link'
22 mimeType: 'video/mp4' | 'video/webm' | 'video/ogg' 22 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
23 mimeType?: 'video/mp4' | 'video/webm' | 'video/ogg'
24 mediaType: 'video/mp4' | 'video/webm' | 'video/ogg'
23 href: string 25 href: string
24 height: number 26 height: number
25 size: number 27 size: number
26 fps: number 28 fps: number
27} 29}
28 30
29export type ActivityUrlObject = 31export type ActivityPlaylistSegmentHashesObject = {
30 ActivityVideoUrlObject 32 type: 'Link'
31 | 33 name: 'sha256'
32 { 34 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
33 type: 'Link' 35 mimeType?: 'application/json'
34 mimeType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet' 36 mediaType: 'application/json'
35 href: string 37 href: string
36 height: number 38}
37 } 39
38 | 40export type ActivityPlaylistInfohashesObject = {
39 { 41 type: 'Infohash'
40 type: 'Link' 42 name: string
41 mimeType: 'text/html' 43}
42 href: string 44
43 } 45export type ActivityPlaylistUrlObject = {
46 type: 'Link'
47 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
48 mimeType?: 'application/x-mpegURL'
49 mediaType: 'application/x-mpegURL'
50 href: string
51 tag?: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
52}
53
54export type ActivityBitTorrentUrlObject = {
55 type: 'Link'
56 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
57 mimeType?: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
58 mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
59 href: string
60 height: number
61}
62
63export type ActivityHtmlUrlObject = {
64 type: 'Link'
65 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
66 mimeType?: 'text/html'
67 mediaType: 'text/html'
68 href: string
69}
70
71export type ActivityUrlObject = ActivityVideoUrlObject | ActivityPlaylistUrlObject | ActivityBitTorrentUrlObject | ActivityHtmlUrlObject
44 72
45export interface ActivityPubAttributedTo { 73export interface ActivityPubAttributedTo {
46 type: 'Group' | 'Person' 74 type: 'Group' | 'Person'
diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts
index 295175774..7218fb784 100644
--- a/shared/models/activitypub/objects/dislike-object.ts
+++ b/shared/models/activitypub/objects/dislike-object.ts
@@ -1,5 +1,6 @@
1export interface DislikeObject { 1export interface DislikeObject {
2 type: 'Dislike', 2 id: string
3 type: 'Dislike'
3 actor: string 4 actor: string
4 object: string 5 object: string
5} 6}
diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts
new file mode 100644
index 000000000..3fd33800a
--- /dev/null
+++ b/shared/models/activitypub/objects/object.model.ts
@@ -0,0 +1 @@
export type APObject = string | { id: string }
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts
index 6b3b1b47c..a3953874d 100644
--- a/shared/models/actors/actor.model.ts
+++ b/shared/models/actors/actor.model.ts
@@ -10,5 +10,5 @@ export interface Actor {
10 followersCount: number 10 followersCount: number
11 createdAt: Date | string 11 createdAt: Date | string
12 updatedAt: Date | string 12 updatedAt: Date | string
13 avatar: Avatar 13 avatar?: Avatar
14} 14}
diff --git a/shared/models/blocklist/account-block.model.ts b/shared/models/blocklist/account-block.model.ts
new file mode 100644
index 000000000..a942ed614
--- /dev/null
+++ b/shared/models/blocklist/account-block.model.ts
@@ -0,0 +1,7 @@
1import { Account } from '../actors'
2
3export interface AccountBlock {
4 byAccount: Account
5 blockedAccount: Account
6 createdAt: Date | string
7}
diff --git a/shared/models/blocklist/index.ts b/shared/models/blocklist/index.ts
new file mode 100644
index 000000000..fc7873270
--- /dev/null
+++ b/shared/models/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './account-block.model'
2export * from './server-block.model'
diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/blocklist/server-block.model.ts
new file mode 100644
index 000000000..a8b8af0b7
--- /dev/null
+++ b/shared/models/blocklist/server-block.model.ts
@@ -0,0 +1,9 @@
1import { Account } from '../actors'
2
3export interface ServerBlock {
4 byAccount: Account
5 blockedServer: {
6 host: string
7 }
8 createdAt: Date | string
9}
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
index 5c3249452..d7164b73f 100644
--- a/shared/models/i18n/i18n.ts
+++ b/shared/models/i18n/i18n.ts
@@ -8,12 +8,14 @@ export const I18N_LOCALES = {
8 'cs-CZ': 'Čeština', 8 'cs-CZ': 'Čeština',
9 'eo': 'Esperanto', 9 'eo': 'Esperanto',
10 'de-DE': 'Deutsch', 10 'de-DE': 'Deutsch',
11 'it-IT': 'Italiano',
11 'es-ES': 'Español', 12 'es-ES': 'Español',
12 'oc': 'Occitan', 13 'oc': 'Occitan',
13 'zh-Hant-TW': '繁體中文(台灣)', 14 'zh-Hant-TW': '繁體中文(台灣)',
14 'pt-BR': 'Português (Brasil)', 15 'pt-BR': 'Português (Brasil)',
15 'sv-SE': 'svenska', 16 'sv-SE': 'svenska',
16 // 'pl-PL': 'Polski' 17 'pl-PL': 'Polski',
18 'ru-RU': 'русский',
17 'zh-Hans-CN': '简体中文(中国)' 19 'zh-Hans-CN': '简体中文(中国)'
18} 20}
19 21
@@ -26,8 +28,9 @@ const I18N_LOCALE_ALIAS = {
26 'de': 'de-DE', 28 'de': 'de-DE',
27 'es': 'es-ES', 29 'es': 'es-ES',
28 'pt': 'pt-BR', 30 'pt': 'pt-BR',
29 'sv': 'sv-SE' 31 'sv': 'sv-SE',
30 // 'pl': 'pl-PL' 32 'pl': 'pl-PL',
33 'ru': 'ru-RU'
31} 34}
32 35
33export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) 36export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
diff --git a/shared/models/index.ts b/shared/models/index.ts
index e61d6cbdc..062533834 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -1,6 +1,7 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './actors' 2export * from './actors'
3export * from './avatars' 3export * from './avatars'
4export * from './blocklist'
4export * from './redundancy' 5export * from './redundancy'
5export * from './users' 6export * from './users'
6export * from './videos' 7export * from './videos'
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts
index 29aa5c100..0db220758 100644
--- a/shared/models/search/videos-search-query.model.ts
+++ b/shared/models/search/videos-search-query.model.ts
@@ -1,4 +1,5 @@
1import { NSFWQuery } from './nsfw-query.model' 1import { NSFWQuery } from './nsfw-query.model'
2import { VideoFilter } from '../videos'
2 3
3export interface VideosSearchQuery { 4export interface VideosSearchQuery {
4 search?: string 5 search?: string
@@ -23,4 +24,6 @@ export interface VideosSearchQuery {
23 24
24 durationMin?: number // seconds 25 durationMin?: number // seconds
25 durationMax?: number // seconds 26 durationMax?: number // seconds
27
28 filter?: VideoFilter
26} 29}
diff --git a/shared/models/server/contact-form.model.ts b/shared/models/server/contact-form.model.ts
new file mode 100644
index 000000000..0696be8b4
--- /dev/null
+++ b/shared/models/server/contact-form.model.ts
@@ -0,0 +1,5 @@
1export interface ContactForm {
2 fromEmail: string
3 fromName: string
4 body: string
5}
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 3afd36fcd..b42ff90c6 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -41,6 +41,10 @@ export interface CustomConfig {
41 email: string 41 email: string
42 } 42 }
43 43
44 contactForm: {
45 enabled: boolean
46 }
47
44 user: { 48 user: {
45 videoQuota: number 49 videoQuota: number
46 videoQuotaDaily: number 50 videoQuotaDaily: number
@@ -48,6 +52,7 @@ export interface CustomConfig {
48 52
49 transcoding: { 53 transcoding: {
50 enabled: boolean 54 enabled: boolean
55 allowAdditionalExtensions: boolean
51 threads: number 56 threads: number
52 resolutions: { 57 resolutions: {
53 '240p': boolean 58 '240p': boolean
@@ -56,6 +61,9 @@ export interface CustomConfig {
56 '720p': boolean 61 '720p': boolean
57 '1080p': boolean 62 '1080p': boolean
58 } 63 }
64 hls: {
65 enabled: boolean
66 }
59 } 67 }
60 68
61 import: { 69 import: {
diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts
new file mode 100644
index 000000000..c42f6f67f
--- /dev/null
+++ b/shared/models/server/index.ts
@@ -0,0 +1,6 @@
1export * from './about.model'
2export * from './contact-form.model'
3export * from './custom-config.model'
4export * from './job.model'
5export * from './server-config.model'
6export * from './server-stats.model'
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index 4046297c4..85bc9541b 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -8,7 +8,8 @@ export type JobType = 'activitypub-http-unicast' |
8 'video-file' | 8 'video-file' |
9 'email' | 9 'email' |
10 'video-import' | 10 'video-import' |
11 'videos-views' 11 'videos-views' |
12 'activitypub-refresher'
12 13
13export interface Job { 14export interface Job {
14 id: number 15 id: number
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 91196c1eb..baafed31f 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -15,13 +15,25 @@ export interface ServerConfig {
15 } 15 }
16 } 16 }
17 17
18 email: {
19 enabled: boolean
20 }
21
22 contactForm: {
23 enabled: boolean
24 }
25
18 signup: { 26 signup: {
19 allowed: boolean, 27 allowed: boolean,
20 allowedForCurrentIP: boolean, 28 allowedForCurrentIP: boolean
21 requiresEmailVerification: boolean 29 requiresEmailVerification: boolean
22 } 30 }
23 31
24 transcoding: { 32 transcoding: {
33 hls: {
34 enabled: boolean
35 }
36
25 enabledResolutions: number[] 37 enabledResolutions: number[]
26 } 38 }
27 39
@@ -40,7 +52,7 @@ export interface ServerConfig {
40 file: { 52 file: {
41 size: { 53 size: {
42 max: number 54 max: number
43 }, 55 }
44 extensions: string[] 56 extensions: string[]
45 } 57 }
46 } 58 }
@@ -70,4 +82,10 @@ export interface ServerConfig {
70 videoQuota: number 82 videoQuota: number
71 videoQuotaDaily: number 83 videoQuotaDaily: number
72 } 84 }
85
86 trending: {
87 videos: {
88 intervalDays: number
89 }
90 }
73} 91}
diff --git a/shared/models/server/server-stats.model.ts b/shared/models/server/server-stats.model.ts
index a6bd2d4d3..74f3de5d3 100644
--- a/shared/models/server/server-stats.model.ts
+++ b/shared/models/server/server-stats.model.ts
@@ -5,6 +5,7 @@ export interface ServerStats {
5 totalLocalVideos: number 5 totalLocalVideos: number
6 totalLocalVideoViews: number 6 totalLocalVideoViews: number
7 totalLocalVideoComments: number 7 totalLocalVideoComments: number
8 totalLocalVideoFilesSize: number
8 9
9 totalVideos: number 10 totalVideos: number
10 totalVideoComments: number 11 totalVideoComments: number
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index 7114741e0..cd07cf320 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -1,6 +1,8 @@
1export * from './user.model' 1export * from './user.model'
2export * from './user-create.model' 2export * from './user-create.model'
3export * from './user-login.model' 3export * from './user-login.model'
4export * from './user-notification.model'
5export * from './user-notification-setting.model'
4export * from './user-refresh-token.model' 6export * from './user-refresh-token.model'
5export * from './user-update.model' 7export * from './user-update.model'
6export * from './user-update-me.model' 8export * from './user-update-me.model'
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
new file mode 100644
index 000000000..531e12bba
--- /dev/null
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -0,0 +1,17 @@
1export enum UserNotificationSettingValue {
2 NONE = 0,
3 WEB = 1 << 0,
4 EMAIL = 1 << 1
5}
6
7export interface UserNotificationSetting {
8 newVideoFromSubscription: UserNotificationSettingValue
9 newCommentOnMyVideo: UserNotificationSettingValue
10 videoAbuseAsModerator: UserNotificationSettingValue
11 blacklistOnMyVideo: UserNotificationSettingValue
12 myVideoPublished: UserNotificationSettingValue
13 myVideoImportFinished: UserNotificationSettingValue
14 newUserRegistration: UserNotificationSettingValue
15 newFollow: UserNotificationSettingValue
16 commentMention: UserNotificationSettingValue
17}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
new file mode 100644
index 000000000..186b62612
--- /dev/null
+++ b/shared/models/users/user-notification.model.ts
@@ -0,0 +1,83 @@
1export enum UserNotificationType {
2 NEW_VIDEO_FROM_SUBSCRIPTION = 1,
3 NEW_COMMENT_ON_MY_VIDEO = 2,
4 NEW_VIDEO_ABUSE_FOR_MODERATORS = 3,
5
6 BLACKLIST_ON_MY_VIDEO = 4,
7 UNBLACKLIST_ON_MY_VIDEO = 5,
8
9 MY_VIDEO_PUBLISHED = 6,
10
11 MY_VIDEO_IMPORT_SUCCESS = 7,
12 MY_VIDEO_IMPORT_ERROR = 8,
13
14 NEW_USER_REGISTRATION = 9,
15 NEW_FOLLOW = 10,
16 COMMENT_MENTION = 11
17}
18
19export interface VideoInfo {
20 id: number
21 uuid: string
22 name: string
23}
24
25export interface ActorInfo {
26 id: number
27 displayName: string
28 name: string
29 host: string
30 avatar?: {
31 path: string
32 }
33}
34
35export interface UserNotification {
36 id: number
37 type: UserNotificationType
38 read: boolean
39
40 video?: VideoInfo & {
41 channel: ActorInfo
42 }
43
44 videoImport?: {
45 id: number
46 video?: VideoInfo
47 torrentName?: string
48 magnetUri?: string
49 targetUrl?: string
50 }
51
52 comment?: {
53 id: number
54 threadId: number
55 account: ActorInfo
56 video: VideoInfo
57 }
58
59 videoAbuse?: {
60 id: number
61 video: VideoInfo
62 }
63
64 videoBlacklist?: {
65 id: number
66 video: VideoInfo
67 }
68
69 account?: ActorInfo
70
71 actorFollow?: {
72 id: number
73 follower: ActorInfo
74 following: {
75 type: 'account' | 'channel'
76 name: string
77 displayName: string
78 }
79 }
80
81 createdAt: string
82 updatedAt: string
83}
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index c4ccd632f..090256bca 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -2,17 +2,26 @@ export enum UserRight {
2 ALL, 2 ALL,
3 3
4 MANAGE_USERS, 4 MANAGE_USERS,
5
5 MANAGE_SERVER_FOLLOW, 6 MANAGE_SERVER_FOLLOW,
7
6 MANAGE_SERVER_REDUNDANCY, 8 MANAGE_SERVER_REDUNDANCY,
9
7 MANAGE_VIDEO_ABUSES, 10 MANAGE_VIDEO_ABUSES,
11
8 MANAGE_JOBS, 12 MANAGE_JOBS,
13
9 MANAGE_CONFIGURATION, 14 MANAGE_CONFIGURATION,
10 15
16 MANAGE_ACCOUNTS_BLOCKLIST,
17 MANAGE_SERVERS_BLOCKLIST,
18
11 MANAGE_VIDEO_BLACKLIST, 19 MANAGE_VIDEO_BLACKLIST,
12 20
13 REMOVE_ANY_VIDEO, 21 REMOVE_ANY_VIDEO,
14 REMOVE_ANY_VIDEO_CHANNEL, 22 REMOVE_ANY_VIDEO_CHANNEL,
15 REMOVE_ANY_VIDEO_COMMENT, 23 REMOVE_ANY_VIDEO_COMMENT,
16 UPDATE_ANY_VIDEO, 24 UPDATE_ANY_VIDEO,
25 SEE_ALL_VIDEOS,
17 CHANGE_VIDEO_OWNERSHIP 26 CHANGE_VIDEO_OWNERSHIP
18} 27}
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts
index 552aad999..59c2ba106 100644
--- a/shared/models/users/user-role.ts
+++ b/shared/models/users/user-role.ts
@@ -26,7 +26,11 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
26 UserRight.REMOVE_ANY_VIDEO, 26 UserRight.REMOVE_ANY_VIDEO,
27 UserRight.REMOVE_ANY_VIDEO_CHANNEL, 27 UserRight.REMOVE_ANY_VIDEO_CHANNEL,
28 UserRight.REMOVE_ANY_VIDEO_COMMENT, 28 UserRight.REMOVE_ANY_VIDEO_COMMENT,
29 UserRight.UPDATE_ANY_VIDEO 29 UserRight.UPDATE_ANY_VIDEO,
30 UserRight.SEE_ALL_VIDEOS,
31 UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
32 UserRight.MANAGE_SERVERS_BLOCKLIST,
33 UserRight.MANAGE_USERS
30 ], 34 ],
31 35
32 [UserRole.USER]: [] 36 [UserRole.USER]: []
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts
index bbffe1487..e24afab94 100644
--- a/shared/models/users/user-update-me.model.ts
+++ b/shared/models/users/user-update-me.model.ts
@@ -4,7 +4,11 @@ export interface UserUpdateMe {
4 displayName?: string 4 displayName?: string
5 description?: string 5 description?: string
6 nsfwPolicy?: NSFWPolicyType 6 nsfwPolicy?: NSFWPolicyType
7
8 webTorrentEnabled?: boolean
7 autoPlayVideo?: boolean 9 autoPlayVideo?: boolean
10 videosHistoryEnabled?: boolean
11
8 email?: string 12 email?: string
9 currentPassword?: string 13 currentPassword?: string
10 password?: string 14 password?: string
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts
index ce866fb18..cd215bab3 100644
--- a/shared/models/users/user-update.model.ts
+++ b/shared/models/users/user-update.model.ts
@@ -1,7 +1,9 @@
1import { UserRole } from './user-role' 1import { UserRole } from './user-role'
2 2
3export interface UserUpdate { 3export interface UserUpdate {
4 password?: string
4 email?: string 5 email?: string
6 emailVerified?: boolean
5 videoQuota?: number 7 videoQuota?: number
6 videoQuotaDaily?: number 8 videoQuotaDaily?: number
7 role?: UserRole 9 role?: UserRole
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index 8147dc48e..af783d389 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -2,18 +2,25 @@ import { Account } from '../actors'
2import { VideoChannel } from '../videos/channel/video-channel.model' 2import { VideoChannel } from '../videos/channel/video-channel.model'
3import { UserRole } from './user-role' 3import { UserRole } from './user-role'
4import { NSFWPolicyType } from '../videos/nsfw-policy.type' 4import { NSFWPolicyType } from '../videos/nsfw-policy.type'
5import { UserNotificationSetting } from './user-notification-setting.model'
5 6
6export interface User { 7export interface User {
7 id: number 8 id: number
8 username: string 9 username: string
9 email: string 10 email: string
11 emailVerified: boolean
10 nsfwPolicy: NSFWPolicyType 12 nsfwPolicy: NSFWPolicyType
13
11 autoPlayVideo: boolean 14 autoPlayVideo: boolean
15 webTorrentEnabled: boolean
16 videosHistoryEnabled: boolean
17
12 role: UserRole 18 role: UserRole
13 videoQuota: number 19 videoQuota: number
14 videoQuotaDaily: number 20 videoQuotaDaily: number
15 createdAt: Date 21 createdAt: Date
16 account: Account 22 account: Account
23 notificationSettings?: UserNotificationSetting
17 videoChannels?: VideoChannel[] 24 videoChannels?: VideoChannel[]
18 25
19 blocked: boolean 26 blocked: boolean
diff --git a/shared/models/videos/blacklist/video-blacklist-create.model.ts b/shared/models/videos/blacklist/video-blacklist-create.model.ts
index 89c69cb56..6e7d36421 100644
--- a/shared/models/videos/blacklist/video-blacklist-create.model.ts
+++ b/shared/models/videos/blacklist/video-blacklist-create.model.ts
@@ -1,3 +1,4 @@
1export interface VideoBlacklistCreate { 1export interface VideoBlacklistCreate {
2 reason?: string 2 reason?: string
3 unfederate?: boolean
3} 4}
diff --git a/shared/models/videos/blacklist/video-blacklist.model.ts b/shared/models/videos/blacklist/video-blacklist.model.ts
index ef4e5e3a2..4bd976190 100644
--- a/shared/models/videos/blacklist/video-blacklist.model.ts
+++ b/shared/models/videos/blacklist/video-blacklist.model.ts
@@ -2,6 +2,7 @@ export interface VideoBlacklist {
2 id: number 2 id: number
3 createdAt: Date 3 createdAt: Date
4 updatedAt: Date 4 updatedAt: Date
5 unfederated: boolean
5 reason?: string 6 reason?: string
6 7
7 video: { 8 video: {
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 90a0e3053..056ae06da 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -21,6 +21,7 @@ export * from './video-update.model'
21export * from './video.model' 21export * from './video.model'
22export * from './video-query.type' 22export * from './video-query.type'
23export * from './video-state.enum' 23export * from './video-state.enum'
24export * from './video-transcoding-fps.model'
24export * from './caption/video-caption.model' 25export * from './caption/video-caption.model'
25export * from './caption/video-caption-update.model' 26export * from './caption/video-caption-update.model'
26export * from './import/video-import-create.model' 27export * from './import/video-import-create.model'
diff --git a/shared/models/videos/video-query.type.ts b/shared/models/videos/video-query.type.ts
index ff0f527f3..f76a91aad 100644
--- a/shared/models/videos/video-query.type.ts
+++ b/shared/models/videos/video-query.type.ts
@@ -1 +1 @@
export type VideoFilter = 'local' export type VideoFilter = 'local' | 'all-local'
diff --git a/shared/models/videos/video-rate.type.ts b/shared/models/videos/video-rate.type.ts
index 17aaba5a5..d48774a4b 100644
--- a/shared/models/videos/video-rate.type.ts
+++ b/shared/models/videos/video-rate.type.ts
@@ -1 +1 @@
export type VideoRateType = 'like' | 'dislike' | 'none' export type VideoRateType = 'like' | 'dislike'
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts
index 100fc0e6e..7da5e7100 100644
--- a/shared/models/videos/video-resolution.enum.ts
+++ b/shared/models/videos/video-resolution.enum.ts
@@ -1,3 +1,5 @@
1import { VideoTranscodingFPS } from './video-transcoding-fps.model'
2
1export enum VideoResolution { 3export enum VideoResolution {
2 H_240P = 240, 4 H_240P = 240,
3 H_360P = 360, 5 H_360P = 360,
@@ -5,3 +7,69 @@ export enum VideoResolution {
5 H_720P = 720, 7 H_720P = 720,
6 H_1080P = 1080 8 H_1080P = 1080
7} 9}
10
11/**
12 * Bitrate targets for different resolutions, at VideoTranscodingFPS.AVERAGE.
13 *
14 * Sources for individual quality levels:
15 * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en
16 * YouTube Video Info (tested with random music video): https://www.h3xed.com/blogmedia/youtube-info.php
17 */
18function getBaseBitrate (resolution: VideoResolution) {
19 switch (resolution) {
20 case VideoResolution.H_240P:
21 // quality according to Google Live Encoder: 300 - 700 Kbps
22 // Quality according to YouTube Video Info: 186 Kbps
23 return 250 * 1000
24 case VideoResolution.H_360P:
25 // quality according to Google Live Encoder: 400 - 1,000 Kbps
26 // Quality according to YouTube Video Info: 480 Kbps
27 return 500 * 1000
28 case VideoResolution.H_480P:
29 // quality according to Google Live Encoder: 500 - 2,000 Kbps
30 // Quality according to YouTube Video Info: 879 Kbps
31 return 900 * 1000
32 case VideoResolution.H_720P:
33 // quality according to Google Live Encoder: 1,500 - 4,000 Kbps
34 // Quality according to YouTube Video Info: 1752 Kbps
35 return 1750 * 1000
36 case VideoResolution.H_1080P: // fallthrough
37 default:
38 // quality according to Google Live Encoder: 3000 - 6000 Kbps
39 // Quality according to YouTube Video Info: 3277 Kbps
40 return 3300 * 1000
41 }
42}
43
44/**
45 * Calculate the target bitrate based on video resolution and FPS.
46 *
47 * The calculation is based on two values:
48 * Bitrate at VideoTranscodingFPS.AVERAGE is always the same as
49 * getBaseBitrate(). Bitrate at VideoTranscodingFPS.MAX is always
50 * getBaseBitrate() * 1.4. All other values are calculated linearly
51 * between these two points.
52 */
53export function getTargetBitrate (resolution: VideoResolution, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) {
54 const baseBitrate = getBaseBitrate(resolution)
55 // The maximum bitrate, used when fps === VideoTranscodingFPS.MAX
56 // Based on numbers from Youtube, 60 fps bitrate divided by 30 fps bitrate:
57 // 720p: 2600 / 1750 = 1.49
58 // 1080p: 4400 / 3300 = 1.33
59 const maxBitrate = baseBitrate * 1.4
60 const maxBitrateDifference = maxBitrate - baseBitrate
61 const maxFpsDifference = fpsTranscodingConstants.MAX - fpsTranscodingConstants.AVERAGE
62 // For 1080p video with default settings, this results in the following formula:
63 // 3300 + (x - 30) * (1320/30)
64 // Example outputs:
65 // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps
66 // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps
67 return baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)
68}
69
70/**
71 * The maximum bitrate we expect to see on a transcoded video in bytes per second.
72 */
73export function getMaxBitrate (resolution: VideoResolution, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) {
74 return getTargetBitrate(resolution, fps, fpsTranscodingConstants) * 2
75}
diff --git a/shared/models/videos/video-streaming-playlist.model.ts b/shared/models/videos/video-streaming-playlist.model.ts
new file mode 100644
index 000000000..17f8fe865
--- /dev/null
+++ b/shared/models/videos/video-streaming-playlist.model.ts
@@ -0,0 +1,12 @@
1import { VideoStreamingPlaylistType } from './video-streaming-playlist.type'
2
3export class VideoStreamingPlaylist {
4 id: number
5 type: VideoStreamingPlaylistType
6 playlistUrl: string
7 segmentsSha256Url: string
8
9 redundancies: {
10 baseUrl: string
11 }[]
12}
diff --git a/shared/models/videos/video-streaming-playlist.type.ts b/shared/models/videos/video-streaming-playlist.type.ts
new file mode 100644
index 000000000..3b403f295
--- /dev/null
+++ b/shared/models/videos/video-streaming-playlist.type.ts
@@ -0,0 +1,3 @@
1export enum VideoStreamingPlaylistType {
2 HLS = 1
3}
diff --git a/shared/models/videos/video-transcoding-fps.model.ts b/shared/models/videos/video-transcoding-fps.model.ts
new file mode 100644
index 000000000..82022d2f1
--- /dev/null
+++ b/shared/models/videos/video-transcoding-fps.model.ts
@@ -0,0 +1,6 @@
1export type VideoTranscodingFPS = {
2 MIN: number,
3 AVERAGE: number,
4 MAX: number,
5 KEEP_ORIGIN_FPS_RESOLUTION_MIN: number
6}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 4a792fcbc..891831a9e 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -5,6 +5,7 @@ import { VideoChannel } from './channel/video-channel.model'
5import { VideoPrivacy } from './video-privacy.enum' 5import { VideoPrivacy } from './video-privacy.enum'
6import { VideoScheduleUpdate } from './video-schedule-update.model' 6import { VideoScheduleUpdate } from './video-schedule-update.model'
7import { VideoConstant } from './video-constant.model' 7import { VideoConstant } from './video-constant.model'
8import { VideoStreamingPlaylist } from './video-streaming-playlist.model'
8 9
9export interface VideoFile { 10export interface VideoFile {
10 magnetUri: string 11 magnetUri: string
@@ -24,7 +25,7 @@ export interface VideoChannelAttribute {
24 displayName: string 25 displayName: string
25 url: string 26 url: string
26 host: string 27 host: string
27 avatar: Avatar 28 avatar?: Avatar
28} 29}
29 30
30export interface AccountAttribute { 31export interface AccountAttribute {
@@ -34,7 +35,7 @@ export interface AccountAttribute {
34 displayName: string 35 displayName: string
35 url: string 36 url: string
36 host: string 37 host: string
37 avatar: Avatar 38 avatar?: Avatar
38} 39}
39 40
40export interface Video { 41export interface Video {
@@ -87,4 +88,8 @@ export interface VideoDetails extends Video {
87 // Not optional in details (unlike in Video) 88 // Not optional in details (unlike in Video)
88 waitTranscoding: boolean 89 waitTranscoding: boolean
89 state: VideoConstant<VideoState> 90 state: VideoConstant<VideoState>
91
92 trackerUrls: string[]
93
94 streamingPlaylists: VideoStreamingPlaylist[]
90} 95}
diff --git a/shared/utils/cli/cli.ts b/shared/utils/cli/cli.ts
new file mode 100644
index 000000000..54d05e9c6
--- /dev/null
+++ b/shared/utils/cli/cli.ts
@@ -0,0 +1,24 @@
1import { exec } from 'child_process'
2
3import { ServerInfo } from '../server/servers'
4
5function getEnvCli (server?: ServerInfo) {
6 return `NODE_ENV=test NODE_APP_INSTANCE=${server.serverNumber}`
7}
8
9async function execCLI (command: string) {
10 return new Promise<string>((res, rej) => {
11 exec(command, (err, stdout, stderr) => {
12 if (err) return rej(err)
13
14 return res(stdout)
15 })
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 execCLI,
23 getEnvCli
24}
diff --git a/shared/utils/feeds/feeds.ts b/shared/utils/feeds/feeds.ts
new file mode 100644
index 000000000..af6df2b20
--- /dev/null
+++ b/shared/utils/feeds/feeds.ts
@@ -0,0 +1,32 @@
1import * as request from 'supertest'
2
3type FeedType = 'videos' | 'video-comments'
4
5function getXMLfeed (url: string, feed: FeedType, format?: string) {
6 const path = '/feeds/' + feed + '.xml'
7
8 return request(url)
9 .get(path)
10 .query((format) ? { format: format } : {})
11 .set('Accept', 'application/xml')
12 .expect(200)
13 .expect('Content-Type', /xml/)
14}
15
16function getJSONfeed (url: string, feed: FeedType, query: any = {}) {
17 const path = '/feeds/' + feed + '.json'
18
19 return request(url)
20 .get(path)
21 .query(query)
22 .set('Accept', 'application/json')
23 .expect(200)
24 .expect('Content-Type', /json/)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 getXMLfeed,
31 getJSONfeed
32}
diff --git a/shared/utils/index.ts b/shared/utils/index.ts
new file mode 100644
index 000000000..156901372
--- /dev/null
+++ b/shared/utils/index.ts
@@ -0,0 +1,25 @@
1export * from './server/activitypub'
2export * from './cli/cli'
3export * from './server/clients'
4export * from './server/config'
5export * from './server/jobs'
6export * from './users/login'
7export * from './miscs/miscs'
8export * from './miscs/stubs'
9export * from './miscs/sql'
10export * from './server/follows'
11export * from './requests/activitypub'
12export * from './requests/requests'
13export * from './requests/check-api-params'
14export * from './server/servers'
15export * from './videos/services'
16export * from './users/users'
17export * from './videos/video-abuses'
18export * from './videos/video-blacklist'
19export * from './videos/video-channels'
20export * from './videos/video-comments'
21export * from './videos/video-playlists'
22export * from './videos/videos'
23export * from './videos/video-change-ownership'
24export * from './feeds/feeds'
25export * from './search/videos'
diff --git a/shared/utils/miscs/email-child-process.js b/shared/utils/miscs/email-child-process.js
new file mode 100644
index 000000000..40ae37d70
--- /dev/null
+++ b/shared/utils/miscs/email-child-process.js
@@ -0,0 +1,27 @@
1const MailDev = require('maildev')
2
3// must run maildev as forked ChildProcess
4// failed instantiation stops main process with exit code 0
5process.on('message', (msg) => {
6 if (msg.start) {
7 const maildev = new MailDev({
8 ip: '127.0.0.1',
9 smtp: 1025,
10 disableWeb: true,
11 silent: true
12 })
13
14 maildev.on('new', email => {
15 process.send({ email })
16 })
17
18 maildev.listen(err => {
19 if (err) {
20 // cannot send as Error object
21 return process.send({ err: err.message })
22 }
23
24 return process.send({ err: null })
25 })
26 }
27})
diff --git a/shared/utils/miscs/email.ts b/shared/utils/miscs/email.ts
new file mode 100644
index 000000000..f9f1bd95b
--- /dev/null
+++ b/shared/utils/miscs/email.ts
@@ -0,0 +1,64 @@
1import { fork, ChildProcess } from 'child_process'
2
3class MockSmtpServer {
4
5 private static instance: MockSmtpServer
6 private started = false
7 private emailChildProcess: ChildProcess
8 private emails: object[]
9
10 private constructor () {
11 this.emailChildProcess = fork(`${__dirname}/email-child-process`, [])
12
13 this.emailChildProcess.on('message', (msg) => {
14 if (msg.email) {
15 return this.emails.push(msg.email)
16 }
17 })
18
19 process.on('exit', () => this.kill())
20 }
21
22 collectEmails (emailsCollection: object[]) {
23 return new Promise((res, rej) => {
24 if (this.started) {
25 this.emails = emailsCollection
26 return res()
27 }
28
29 // ensure maildev isn't started until
30 // unexpected exit can be reported to test runner
31 this.emailChildProcess.send({ start: true })
32 this.emailChildProcess.on('exit', () => {
33 return rej(new Error('maildev exited unexpectedly, confirm port not in use'))
34 })
35 this.emailChildProcess.on('message', (msg) => {
36 if (msg.err) {
37 return rej(new Error(msg.err))
38 }
39 this.started = true
40 this.emails = emailsCollection
41 return res()
42 })
43 })
44 }
45
46 kill () {
47 if (!this.emailChildProcess) return
48
49 process.kill(this.emailChildProcess.pid)
50
51 this.emailChildProcess = null
52 MockSmtpServer.instance = null
53 }
54
55 static get Instance () {
56 return this.instance || (this.instance = new this())
57 }
58}
59
60// ---------------------------------------------------------------------------
61
62export {
63 MockSmtpServer
64}
diff --git a/shared/utils/miscs/miscs.ts b/shared/utils/miscs/miscs.ts
new file mode 100644
index 000000000..91a93b631
--- /dev/null
+++ b/shared/utils/miscs/miscs.ts
@@ -0,0 +1,101 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import { isAbsolute, join } from 'path'
5import * as request from 'supertest'
6import * as WebTorrent from 'webtorrent'
7import { pathExists, readFile } from 'fs-extra'
8import * as ffmpeg from 'fluent-ffmpeg'
9
10const expect = chai.expect
11let webtorrent = new WebTorrent()
12
13function immutableAssign <T, U> (target: T, source: U) {
14 return Object.assign<{}, T, U>({}, target, source)
15}
16
17 // Default interval -> 5 minutes
18function dateIsValid (dateString: string, interval = 300000) {
19 const dateToCheck = new Date(dateString)
20 const now = new Date()
21
22 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
23}
24
25function wait (milliseconds: number) {
26 return new Promise(resolve => setTimeout(resolve, milliseconds))
27}
28
29function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
30 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
31
32 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
33}
34
35function root () {
36 // We are in /shared/utils/miscs
37 return join(__dirname, '..', '..', '..')
38}
39
40async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
41 const res = await request(url)
42 .get(imagePath)
43 .expect(200)
44
45 const body = res.body
46
47 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
48 const minLength = body.length - ((20 * body.length) / 100)
49 const maxLength = body.length + ((20 * body.length) / 100)
50
51 expect(data.length).to.be.above(minLength)
52 expect(data.length).to.be.below(maxLength)
53}
54
55function buildAbsoluteFixturePath (path: string, customTravisPath = false) {
56 if (isAbsolute(path)) {
57 return path
58 }
59
60 if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path)
61
62 return join(root(), 'server', 'tests', 'fixtures', path)
63}
64
65async function generateHighBitrateVideo () {
66 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
67
68 const exists = await pathExists(tempFixturePath)
69 if (!exists) {
70
71 // Generate a random, high bitrate video on the fly, so we don't have to include
72 // a large file in the repo. The video needs to have a certain minimum length so
73 // that FFmpeg properly applies bitrate limits.
74 // https://stackoverflow.com/a/15795112
75 return new Promise<string>(async (res, rej) => {
76 ffmpeg()
77 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
78 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
79 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
80 .output(tempFixturePath)
81 .on('error', rej)
82 .on('end', () => res(tempFixturePath))
83 .run()
84 })
85 }
86
87 return tempFixturePath
88}
89
90// ---------------------------------------------------------------------------
91
92export {
93 dateIsValid,
94 wait,
95 webtorrentAdd,
96 immutableAssign,
97 testImage,
98 buildAbsoluteFixturePath,
99 root,
100 generateHighBitrateVideo
101}
diff --git a/shared/utils/miscs/sql.ts b/shared/utils/miscs/sql.ts
new file mode 100644
index 000000000..027f78131
--- /dev/null
+++ b/shared/utils/miscs/sql.ts
@@ -0,0 +1,38 @@
1import * as Sequelize from 'sequelize'
2
3function getSequelize (serverNumber: number) {
4 const dbname = 'peertube_test' + serverNumber
5 const username = 'peertube'
6 const password = 'peertube'
7 const host = 'localhost'
8 const port = 5432
9
10 return new Sequelize(dbname, username, password, {
11 dialect: 'postgres',
12 host,
13 port,
14 operatorsAliases: false,
15 logging: false
16 })
17}
18
19function setActorField (serverNumber: number, to: string, field: string, value: string) {
20 const seq = getSequelize(serverNumber)
21
22 const options = { type: Sequelize.QueryTypes.UPDATE }
23
24 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
25}
26
27function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
28 const seq = getSequelize(serverNumber)
29
30 const options = { type: Sequelize.QueryTypes.UPDATE }
31
32 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
33}
34
35export {
36 setVideoField,
37 setActorField
38}
diff --git a/shared/utils/miscs/stubs.ts b/shared/utils/miscs/stubs.ts
new file mode 100644
index 000000000..d1eb0e3b2
--- /dev/null
+++ b/shared/utils/miscs/stubs.ts
@@ -0,0 +1,14 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/shared/utils/overviews/overviews.ts b/shared/utils/overviews/overviews.ts
new file mode 100644
index 000000000..23e3ceb1e
--- /dev/null
+++ b/shared/utils/overviews/overviews.ts
@@ -0,0 +1,18 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getVideosOverview (url: string, useCache = false) {
4 const path = '/api/v1/overviews/videos'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18export { getVideosOverview }
diff --git a/shared/utils/requests/activitypub.ts b/shared/utils/requests/activitypub.ts
new file mode 100644
index 000000000..e2348ace0
--- /dev/null
+++ b/shared/utils/requests/activitypub.ts
@@ -0,0 +1,43 @@
1import { doRequest } from '../../../server/helpers/requests'
2import { HTTP_SIGNATURE } from '../../../server/initializers'
3import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../server/helpers/activitypub'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makePOSTAPRequest,
42 makeFollowRequest
43}
diff --git a/shared/utils/requests/check-api-params.ts b/shared/utils/requests/check-api-params.ts
new file mode 100644
index 000000000..a2a549682
--- /dev/null
+++ b/shared/utils/requests/check-api-params.ts
@@ -0,0 +1,40 @@
1import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3
4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
5 return makeGetRequest({
6 url,
7 path,
8 token,
9 query: immutableAssign(query, { start: 'hello' }),
10 statusCodeExpected: 400
11 })
12}
13
14function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
15 return makeGetRequest({
16 url,
17 path,
18 token,
19 query: immutableAssign(query, { count: 'hello' }),
20 statusCodeExpected: 400
21 })
22}
23
24function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
25 return makeGetRequest({
26 url,
27 path,
28 token,
29 query: immutableAssign(query, { sort: 'hello' }),
30 statusCodeExpected: 400
31 })
32}
33
34// ---------------------------------------------------------------------------
35
36export {
37 checkBadStartPagination,
38 checkBadCountPagination,
39 checkBadSortPagination
40}
diff --git a/shared/utils/requests/requests.ts b/shared/utils/requests/requests.ts
new file mode 100644
index 000000000..6b59e24fc
--- /dev/null
+++ b/shared/utils/requests/requests.ts
@@ -0,0 +1,177 @@
1import * as request from 'supertest'
2import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
3import { isAbsolute, join } from 'path'
4import { parse } from 'url'
5
6function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
7 const { host, protocol, pathname } = parse(url)
8
9 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
10}
11
12function makeGetRequest (options: {
13 url: string,
14 path?: string,
15 query?: any,
16 token?: string,
17 statusCodeExpected?: number,
18 contentType?: string,
19 range?: string
20}) {
21 if (!options.statusCodeExpected) options.statusCodeExpected = 400
22 if (options.contentType === undefined) options.contentType = 'application/json'
23
24 const req = request(options.url).get(options.path)
25
26 if (options.contentType) req.set('Accept', options.contentType)
27 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
28 if (options.query) req.query(options.query)
29 if (options.range) req.set('Range', options.range)
30
31 return req.expect(options.statusCodeExpected)
32}
33
34function makeDeleteRequest (options: {
35 url: string,
36 path: string,
37 token?: string,
38 statusCodeExpected?: number
39}) {
40 if (!options.statusCodeExpected) options.statusCodeExpected = 400
41
42 const req = request(options.url)
43 .delete(options.path)
44 .set('Accept', 'application/json')
45
46 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
47
48 return req.expect(options.statusCodeExpected)
49}
50
51function makeUploadRequest (options: {
52 url: string,
53 method?: 'POST' | 'PUT',
54 path: string,
55 token?: string,
56 fields: { [ fieldName: string ]: any },
57 attaches: { [ attachName: string ]: any | any[] },
58 statusCodeExpected?: number
59}) {
60 if (!options.statusCodeExpected) options.statusCodeExpected = 400
61
62 let req: request.Test
63 if (options.method === 'PUT') {
64 req = request(options.url).put(options.path)
65 } else {
66 req = request(options.url).post(options.path)
67 }
68
69 req.set('Accept', 'application/json')
70
71 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
72
73 Object.keys(options.fields).forEach(field => {
74 const value = options.fields[field]
75
76 if (Array.isArray(value)) {
77 for (let i = 0; i < value.length; i++) {
78 req.field(field + '[' + i + ']', value[i])
79 }
80 } else {
81 req.field(field, value)
82 }
83 })
84
85 Object.keys(options.attaches).forEach(attach => {
86 const value = options.attaches[attach]
87 if (Array.isArray(value)) {
88 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
89 } else {
90 req.attach(attach, buildAbsoluteFixturePath(value))
91 }
92 })
93
94 return req.expect(options.statusCodeExpected)
95}
96
97function makePostBodyRequest (options: {
98 url: string,
99 path: string,
100 token?: string,
101 fields?: { [ fieldName: string ]: any },
102 statusCodeExpected?: number
103}) {
104 if (!options.fields) options.fields = {}
105 if (!options.statusCodeExpected) options.statusCodeExpected = 400
106
107 const req = request(options.url)
108 .post(options.path)
109 .set('Accept', 'application/json')
110
111 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
112
113 return req.send(options.fields)
114 .expect(options.statusCodeExpected)
115}
116
117function makePutBodyRequest (options: {
118 url: string,
119 path: string,
120 token?: string,
121 fields: { [ fieldName: string ]: any },
122 statusCodeExpected?: number
123}) {
124 if (!options.statusCodeExpected) options.statusCodeExpected = 400
125
126 const req = request(options.url)
127 .put(options.path)
128 .set('Accept', 'application/json')
129
130 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
131
132 return req.send(options.fields)
133 .expect(options.statusCodeExpected)
134}
135
136function makeHTMLRequest (url: string, path: string) {
137 return request(url)
138 .get(path)
139 .set('Accept', 'text/html')
140 .expect(200)
141}
142
143function updateAvatarRequest (options: {
144 url: string,
145 path: string,
146 accessToken: string,
147 fixture: string
148}) {
149 let filePath = ''
150 if (isAbsolute(options.fixture)) {
151 filePath = options.fixture
152 } else {
153 filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
154 }
155
156 return makeUploadRequest({
157 url: options.url,
158 path: options.path,
159 token: options.accessToken,
160 fields: {},
161 attaches: { avatarfile: filePath },
162 statusCodeExpected: 200
163 })
164}
165
166// ---------------------------------------------------------------------------
167
168export {
169 makeHTMLRequest,
170 makeGetRequest,
171 makeUploadRequest,
172 makePostBodyRequest,
173 makePutBodyRequest,
174 makeDeleteRequest,
175 makeRawRequest,
176 updateAvatarRequest
177}
diff --git a/shared/utils/search/video-channels.ts b/shared/utils/search/video-channels.ts
new file mode 100644
index 000000000..0532134ae
--- /dev/null
+++ b/shared/utils/search/video-channels.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = 200) {
4 const path = '/api/v1/search/video-channels'
5
6 return makeGetRequest({
7 url,
8 path,
9 query: {
10 sort: '-createdAt',
11 search
12 },
13 token,
14 statusCodeExpected
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 searchVideoChannel
22}
diff --git a/shared/utils/search/videos.ts b/shared/utils/search/videos.ts
new file mode 100644
index 000000000..ba4627017
--- /dev/null
+++ b/shared/utils/search/videos.ts
@@ -0,0 +1,77 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../models/search'
5import { immutableAssign } from '../miscs/miscs'
6
7function searchVideo (url: string, search: string) {
8 const path = '/api/v1/search/videos'
9 const req = request(url)
10 .get(path)
11 .query({ sort: '-publishedAt', search })
12 .set('Accept', 'application/json')
13
14 return req.expect(200)
15 .expect('Content-Type', /json/)
16}
17
18function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
19 const path = '/api/v1/search/videos'
20 const req = request(url)
21 .get(path)
22 .set('Authorization', 'Bearer ' + token)
23 .query(immutableAssign(query, { sort: '-publishedAt', search }))
24 .set('Accept', 'application/json')
25
26 return req.expect(200)
27 .expect('Content-Type', /json/)
28}
29
30function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
31 const path = '/api/v1/search/videos'
32
33 const req = request(url)
34 .get(path)
35 .query({ start })
36 .query({ search })
37 .query({ count })
38
39 if (sort) req.query({ sort })
40
41 return req.set('Accept', 'application/json')
42 .expect(200)
43 .expect('Content-Type', /json/)
44}
45
46function searchVideoWithSort (url: string, search: string, sort: string) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query({ search })
52 .query({ sort })
53 .set('Accept', 'application/json')
54 .expect(200)
55 .expect('Content-Type', /json/)
56}
57
58function advancedVideosSearch (url: string, options: VideosSearchQuery) {
59 const path = '/api/v1/search/videos'
60
61 return request(url)
62 .get(path)
63 .query(options)
64 .set('Accept', 'application/json')
65 .expect(200)
66 .expect('Content-Type', /json/)
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 searchVideo,
73 advancedVideosSearch,
74 searchVideoWithToken,
75 searchVideoWithPagination,
76 searchVideoWithSort
77}
diff --git a/shared/utils/server/activitypub.ts b/shared/utils/server/activitypub.ts
new file mode 100644
index 000000000..eccb198ca
--- /dev/null
+++ b/shared/utils/server/activitypub.ts
@@ -0,0 +1,14 @@
1import * as request from 'supertest'
2
3function makeActivityPubGetRequest (url: string, path: string, expectedStatus = 200) {
4 return request(url)
5 .get(path)
6 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
7 .expect(expectedStatus)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 makeActivityPubGetRequest
14}
diff --git a/shared/utils/server/clients.ts b/shared/utils/server/clients.ts
new file mode 100644
index 000000000..273aac747
--- /dev/null
+++ b/shared/utils/server/clients.ts
@@ -0,0 +1,19 @@
1import * as request from 'supertest'
2import * as urlUtil from 'url'
3
4function getClient (url: string) {
5 const path = '/api/v1/oauth-clients/local'
6
7 return request(url)
8 .get(path)
9 .set('Host', urlUtil.parse(url).host)
10 .set('Accept', 'application/json')
11 .expect(200)
12 .expect('Content-Type', /json/)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 getClient
19}
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts
new file mode 100644
index 000000000..29c24cff9
--- /dev/null
+++ b/shared/utils/server/config.ts
@@ -0,0 +1,142 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3
4function getConfig (url: string) {
5 const path = '/api/v1/config'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected: 200
11 })
12}
13
14function getAbout (url: string) {
15 const path = '/api/v1/config/about'
16
17 return makeGetRequest({
18 url,
19 path,
20 statusCodeExpected: 200
21 })
22}
23
24function getCustomConfig (url: string, token: string, statusCodeExpected = 200) {
25 const path = '/api/v1/config/custom'
26
27 return makeGetRequest({
28 url,
29 token,
30 path,
31 statusCodeExpected
32 })
33}
34
35function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = 200) {
36 const path = '/api/v1/config/custom'
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: newCustomConfig,
43 statusCodeExpected
44 })
45}
46
47function updateCustomSubConfig (url: string, token: string, newConfig: any) {
48 const updateParams: CustomConfig = {
49 instance: {
50 name: 'PeerTube updated',
51 shortDescription: 'my short description',
52 description: 'my super description',
53 terms: 'my super terms',
54 defaultClientRoute: '/videos/recently-added',
55 defaultNSFWPolicy: 'blur',
56 customizations: {
57 javascript: 'alert("coucou")',
58 css: 'body { background-color: red; }'
59 }
60 },
61 services: {
62 twitter: {
63 username: '@MySuperUsername',
64 whitelisted: true
65 }
66 },
67 cache: {
68 previews: {
69 size: 2
70 },
71 captions: {
72 size: 3
73 }
74 },
75 signup: {
76 enabled: false,
77 limit: 5,
78 requiresEmailVerification: false
79 },
80 admin: {
81 email: 'superadmin1@example.com'
82 },
83 contactForm: {
84 enabled: true
85 },
86 user: {
87 videoQuota: 5242881,
88 videoQuotaDaily: 318742
89 },
90 transcoding: {
91 enabled: true,
92 allowAdditionalExtensions: true,
93 threads: 1,
94 resolutions: {
95 '240p': false,
96 '360p': true,
97 '480p': true,
98 '720p': false,
99 '1080p': false
100 },
101 hls: {
102 enabled: false
103 }
104 },
105 import: {
106 videos: {
107 http: {
108 enabled: false
109 },
110 torrent: {
111 enabled: false
112 }
113 }
114 }
115 }
116
117 Object.assign(updateParams, newConfig)
118
119 return updateCustomConfig(url, token, updateParams)
120}
121
122function deleteCustomConfig (url: string, token: string, statusCodeExpected = 200) {
123 const path = '/api/v1/config/custom'
124
125 return makeDeleteRequest({
126 url,
127 token,
128 path,
129 statusCodeExpected
130 })
131}
132
133// ---------------------------------------------------------------------------
134
135export {
136 getConfig,
137 getCustomConfig,
138 updateCustomConfig,
139 getAbout,
140 deleteCustomConfig,
141 updateCustomSubConfig
142}
diff --git a/shared/utils/server/contact-form.ts b/shared/utils/server/contact-form.ts
new file mode 100644
index 000000000..80394cf99
--- /dev/null
+++ b/shared/utils/server/contact-form.ts
@@ -0,0 +1,28 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3
4function sendContactForm (options: {
5 url: string,
6 fromEmail: string,
7 fromName: string,
8 body: string,
9 expectedStatus?: number
10}) {
11 const path = '/api/v1/server/contact'
12
13 const body: ContactForm = {
14 fromEmail: options.fromEmail,
15 fromName: options.fromName,
16 body: options.body
17 }
18 return request(options.url)
19 .post(path)
20 .send(body)
21 .expect(options.expectedStatus || 204)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 sendContactForm
28}
diff --git a/shared/utils/server/follows.ts b/shared/utils/server/follows.ts
new file mode 100644
index 000000000..7741757a6
--- /dev/null
+++ b/shared/utils/server/follows.ts
@@ -0,0 +1,79 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs'
4
5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
6 const path = '/api/v1/server/followers'
7
8 return request(url)
9 .get(path)
10 .query({ start })
11 .query({ count })
12 .query({ sort })
13 .query({ search })
14 .set('Accept', 'application/json')
15 .expect(200)
16 .expect('Content-Type', /json/)
17}
18
19function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
20 const path = '/api/v1/server/following'
21
22 return request(url)
23 .get(path)
24 .query({ start })
25 .query({ count })
26 .query({ sort })
27 .query({ search })
28 .set('Accept', 'application/json')
29 .expect(200)
30 .expect('Content-Type', /json/)
31}
32
33async function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) {
34 const path = '/api/v1/server/following'
35
36 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
37 const res = await request(follower)
38 .post(path)
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + accessToken)
41 .send({ 'hosts': followingHosts })
42 .expect(expectedStatus)
43
44 return res
45}
46
47async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
48 const path = '/api/v1/server/following/' + target.host
49
50 const res = await request(url)
51 .delete(path)
52 .set('Accept', 'application/json')
53 .set('Authorization', 'Bearer ' + accessToken)
54 .expect(expectedStatus)
55
56 return res
57}
58
59async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
60 await Promise.all([
61 follow(server1.url, [ server2.url ], server1.accessToken),
62 follow(server2.url, [ server1.url ], server2.accessToken)
63 ])
64
65 // Wait request propagation
66 await waitJobs([ server1, server2 ])
67
68 return true
69}
70
71// ---------------------------------------------------------------------------
72
73export {
74 getFollowersListPaginationAndSort,
75 getFollowingListPaginationAndSort,
76 unfollow,
77 follow,
78 doubleFollow
79}
diff --git a/shared/utils/server/jobs.ts b/shared/utils/server/jobs.ts
new file mode 100644
index 000000000..692b5e24d
--- /dev/null
+++ b/shared/utils/server/jobs.ts
@@ -0,0 +1,82 @@
1import * as request from 'supertest'
2import { Job, JobState } from '../../models'
3import { wait } from '../miscs/miscs'
4import { ServerInfo } from './servers'
5
6function getJobsList (url: string, accessToken: string, state: JobState) {
7 const path = '/api/v1/jobs/' + state
8
9 return request(url)
10 .get(path)
11 .set('Accept', 'application/json')
12 .set('Authorization', 'Bearer ' + accessToken)
13 .expect(200)
14 .expect('Content-Type', /json/)
15}
16
17function getJobsListPaginationAndSort (url: string, accessToken: string, state: JobState, start: number, count: number, sort: string) {
18 const path = '/api/v1/jobs/' + state
19
20 return request(url)
21 .get(path)
22 .query({ start })
23 .query({ count })
24 .query({ sort })
25 .set('Accept', 'application/json')
26 .set('Authorization', 'Bearer ' + accessToken)
27 .expect(200)
28 .expect('Content-Type', /json/)
29}
30
31async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
32 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) : 2000
33 let servers: ServerInfo[]
34
35 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ]
36 else servers = serversArg as ServerInfo[]
37
38 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
39 let pendingRequests = false
40
41 function tasksBuilder () {
42 const tasks: Promise<any>[] = []
43 pendingRequests = false
44
45 // Check if each server has pending request
46 for (const server of servers) {
47 for (const state of states) {
48 const p = getJobsListPaginationAndSort(server.url, server.accessToken, state, 0, 10, '-createdAt')
49 .then(res => res.body.data)
50 .then((jobs: Job[]) => jobs.filter(j => j.type !== 'videos-views'))
51 .then(jobs => {
52 if (jobs.length !== 0) pendingRequests = true
53 })
54 tasks.push(p)
55 }
56 }
57
58 return tasks
59 }
60
61 do {
62 await Promise.all(tasksBuilder())
63
64 // Retry, in case of new jobs were created
65 if (pendingRequests === false) {
66 await wait(pendingJobWait)
67 await Promise.all(tasksBuilder())
68 }
69
70 if (pendingRequests) {
71 await wait(1000)
72 }
73 } while (pendingRequests)
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 getJobsList,
80 waitJobs,
81 getJobsListPaginationAndSort
82}
diff --git a/shared/utils/server/redundancy.ts b/shared/utils/server/redundancy.ts
new file mode 100644
index 000000000..c39ff2c8b
--- /dev/null
+++ b/shared/utils/server/redundancy.ts
@@ -0,0 +1,17 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3async function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) {
4 const path = '/api/v1/server/redundancy/' + host
5
6 return makePutBodyRequest({
7 url,
8 path,
9 token: accessToken,
10 fields: { redundancyAllowed },
11 statusCodeExpected: expectedStatus
12 })
13}
14
15export {
16 updateRedundancy
17}
diff --git a/shared/utils/server/servers.ts b/shared/utils/server/servers.ts
new file mode 100644
index 000000000..bde7dd5c2
--- /dev/null
+++ b/shared/utils/server/servers.ts
@@ -0,0 +1,215 @@
1/* tslint:disable:no-unused-expression */
2
3import { ChildProcess, exec, fork } from 'child_process'
4import { join } from 'path'
5import { root, wait } from '../miscs/miscs'
6import { readdir, readFile } from 'fs-extra'
7import { existsSync } from 'fs'
8import { expect } from 'chai'
9
10interface ServerInfo {
11 app: ChildProcess,
12 url: string
13 host: string
14 serverNumber: number
15
16 client: {
17 id: string,
18 secret: string
19 }
20
21 user: {
22 username: string,
23 password: string,
24 email?: string
25 }
26
27 accessToken?: string
28
29 video?: {
30 id: number
31 uuid: string
32 name: string
33 account: {
34 name: string
35 }
36 }
37
38 remoteVideo?: {
39 id: number
40 uuid: string
41 }
42}
43
44function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
45 let apps = []
46 let i = 0
47
48 return new Promise<ServerInfo[]>(res => {
49 function anotherServerDone (serverNumber, app) {
50 apps[serverNumber - 1] = app
51 i++
52 if (i === totalServers) {
53 return res(apps)
54 }
55 }
56
57 flushTests()
58 .then(() => {
59 for (let j = 1; j <= totalServers; j++) {
60 runServer(j, configOverride).then(app => anotherServerDone(j, app))
61 }
62 })
63 })
64}
65
66function flushTests () {
67 return new Promise<void>((res, rej) => {
68 return exec('npm run clean:server:test', err => {
69 if (err) return rej(err)
70
71 return res()
72 })
73 })
74}
75
76function runServer (serverNumber: number, configOverride?: Object, args = []) {
77 const server: ServerInfo = {
78 app: null,
79 serverNumber: serverNumber,
80 url: `http://localhost:${9000 + serverNumber}`,
81 host: `localhost:${9000 + serverNumber}`,
82 client: {
83 id: null,
84 secret: null
85 },
86 user: {
87 username: null,
88 password: null
89 }
90 }
91
92 // These actions are async so we need to be sure that they have both been done
93 const serverRunString = {
94 'Server listening': false
95 }
96 const key = 'Database peertube_test' + serverNumber + ' is ready'
97 serverRunString[key] = false
98
99 const regexps = {
100 client_id: 'Client id: (.+)',
101 client_secret: 'Client secret: (.+)',
102 user_username: 'Username: (.+)',
103 user_password: 'User password: (.+)'
104 }
105
106 // Share the environment
107 const env = Object.create(process.env)
108 env['NODE_ENV'] = 'test'
109 env['NODE_APP_INSTANCE'] = serverNumber.toString()
110
111 if (configOverride !== undefined) {
112 env['NODE_CONFIG'] = JSON.stringify(configOverride)
113 }
114
115 const options = {
116 silent: true,
117 env: env,
118 detached: true
119 }
120
121 return new Promise<ServerInfo>(res => {
122 server.app = fork(join(root(), 'dist', 'server.js'), args, options)
123 server.app.stdout.on('data', function onStdout (data) {
124 let dontContinue = false
125
126 // Capture things if we want to
127 for (const key of Object.keys(regexps)) {
128 const regexp = regexps[key]
129 const matches = data.toString().match(regexp)
130 if (matches !== null) {
131 if (key === 'client_id') server.client.id = matches[1]
132 else if (key === 'client_secret') server.client.secret = matches[1]
133 else if (key === 'user_username') server.user.username = matches[1]
134 else if (key === 'user_password') server.user.password = matches[1]
135 }
136 }
137
138 // Check if all required sentences are here
139 for (const key of Object.keys(serverRunString)) {
140 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
141 if (serverRunString[key] === false) dontContinue = true
142 }
143
144 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
145 if (dontContinue === true) return
146
147 server.app.stdout.removeListener('data', onStdout)
148
149 process.on('exit', () => {
150 try {
151 process.kill(server.app.pid)
152 } catch { /* empty */ }
153 })
154
155 res(server)
156 })
157
158 })
159}
160
161async function reRunServer (server: ServerInfo, configOverride?: any) {
162 const newServer = await runServer(server.serverNumber, configOverride)
163 server.app = newServer.app
164
165 return server
166}
167
168async function checkTmpIsEmpty (server: ServerInfo) {
169 return checkDirectoryIsEmpty(server, 'tmp')
170}
171
172async function checkDirectoryIsEmpty (server: ServerInfo, directory: string) {
173 const testDirectory = 'test' + server.serverNumber
174
175 const directoryPath = join(root(), testDirectory, directory)
176
177 const directoryExists = existsSync(directoryPath)
178 expect(directoryExists).to.be.true
179
180 const files = await readdir(directoryPath)
181 expect(files).to.have.lengthOf(0)
182}
183
184function killallServers (servers: ServerInfo[]) {
185 for (const server of servers) {
186 process.kill(-server.app.pid)
187 }
188}
189
190async function waitUntilLog (server: ServerInfo, str: string, count = 1) {
191 const logfile = join(root(), 'test' + server.serverNumber, 'logs/peertube.log')
192
193 while (true) {
194 const buf = await readFile(logfile)
195
196 const matches = buf.toString().match(new RegExp(str, 'g'))
197 if (matches && matches.length === count) return
198
199 await wait(1000)
200 }
201}
202
203// ---------------------------------------------------------------------------
204
205export {
206 checkDirectoryIsEmpty,
207 checkTmpIsEmpty,
208 ServerInfo,
209 flushAndRunMultipleServers,
210 flushTests,
211 runServer,
212 killallServers,
213 reRunServer,
214 waitUntilLog
215}
diff --git a/shared/utils/server/stats.ts b/shared/utils/server/stats.ts
new file mode 100644
index 000000000..6f079ad18
--- /dev/null
+++ b/shared/utils/server/stats.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getStats (url: string, useCache = false) {
4 const path = '/api/v1/server/stats'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 getStats
22}
diff --git a/shared/utils/socket/socket-io.ts b/shared/utils/socket/socket-io.ts
new file mode 100644
index 000000000..854ab71af
--- /dev/null
+++ b/shared/utils/socket/socket-io.ts
@@ -0,0 +1,13 @@
1import * as io from 'socket.io-client'
2
3function getUserNotificationSocket (serverUrl: string, accessToken: string) {
4 return io(serverUrl + '/user-notifications', {
5 query: { accessToken }
6 })
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 getUserNotificationSocket
13}
diff --git a/shared/utils/users/accounts.ts b/shared/utils/users/accounts.ts
new file mode 100644
index 000000000..388eb6973
--- /dev/null
+++ b/shared/utils/users/accounts.ts
@@ -0,0 +1,63 @@
1/* tslint:disable:no-unused-expression */
2
3import { expect } from 'chai'
4import { existsSync, readdir } from 'fs-extra'
5import { join } from 'path'
6import { Account } from '../../models/actors'
7import { root } from '../miscs/miscs'
8import { makeGetRequest } from '../requests/requests'
9
10function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) {
11 const path = '/api/v1/accounts'
12
13 return makeGetRequest({
14 url,
15 query: { sort },
16 path,
17 statusCodeExpected
18 })
19}
20
21function getAccount (url: string, accountName: string, statusCodeExpected = 200) {
22 const path = '/api/v1/accounts/' + accountName
23
24 return makeGetRequest({
25 url,
26 path,
27 statusCodeExpected
28 })
29}
30
31async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
32 const res = await getAccountsList(url)
33 const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
34
35 const message = `${nameWithDomain} on ${url}`
36 expect(account.followersCount).to.equal(followersCount, message)
37 expect(account.followingCount).to.equal(followingCount, message)
38}
39
40async function checkActorFilesWereRemoved (actorUUID: string, serverNumber: number) {
41 const testDirectory = 'test' + serverNumber
42
43 for (const directory of [ 'avatars' ]) {
44 const directoryPath = join(root(), testDirectory, directory)
45
46 const directoryExists = existsSync(directoryPath)
47 expect(directoryExists).to.be.true
48
49 const files = await readdir(directoryPath)
50 for (const file of files) {
51 expect(file).to.not.contain(actorUUID)
52 }
53 }
54}
55
56// ---------------------------------------------------------------------------
57
58export {
59 getAccount,
60 expectAccountFollows,
61 getAccountsList,
62 checkActorFilesWereRemoved
63}
diff --git a/shared/utils/users/blocklist.ts b/shared/utils/users/blocklist.ts
new file mode 100644
index 000000000..5feb84179
--- /dev/null
+++ b/shared/utils/users/blocklist.ts
@@ -0,0 +1,197 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4
5function getAccountBlocklistByAccount (
6 url: string,
7 token: string,
8 start: number,
9 count: number,
10 sort = '-createdAt',
11 statusCodeExpected = 200
12) {
13 const path = '/api/v1/users/me/blocklist/accounts'
14
15 return makeGetRequest({
16 url,
17 token,
18 query: { start, count, sort },
19 path,
20 statusCodeExpected
21 })
22}
23
24function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
25 const path = '/api/v1/users/me/blocklist/accounts'
26
27 return makePostBodyRequest({
28 url,
29 path,
30 token,
31 fields: {
32 accountName: accountToBlock
33 },
34 statusCodeExpected
35 })
36}
37
38function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
39 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
40
41 return makeDeleteRequest({
42 url,
43 path,
44 token,
45 statusCodeExpected
46 })
47}
48
49function getServerBlocklistByAccount (
50 url: string,
51 token: string,
52 start: number,
53 count: number,
54 sort = '-createdAt',
55 statusCodeExpected = 200
56) {
57 const path = '/api/v1/users/me/blocklist/servers'
58
59 return makeGetRequest({
60 url,
61 token,
62 query: { start, count, sort },
63 path,
64 statusCodeExpected
65 })
66}
67
68function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
69 const path = '/api/v1/users/me/blocklist/servers'
70
71 return makePostBodyRequest({
72 url,
73 path,
74 token,
75 fields: {
76 host: serverToBlock
77 },
78 statusCodeExpected
79 })
80}
81
82function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
83 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
84
85 return makeDeleteRequest({
86 url,
87 path,
88 token,
89 statusCodeExpected
90 })
91}
92
93function getAccountBlocklistByServer (
94 url: string,
95 token: string,
96 start: number,
97 count: number,
98 sort = '-createdAt',
99 statusCodeExpected = 200
100) {
101 const path = '/api/v1/server/blocklist/accounts'
102
103 return makeGetRequest({
104 url,
105 token,
106 query: { start, count, sort },
107 path,
108 statusCodeExpected
109 })
110}
111
112function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
113 const path = '/api/v1/server/blocklist/accounts'
114
115 return makePostBodyRequest({
116 url,
117 path,
118 token,
119 fields: {
120 accountName: accountToBlock
121 },
122 statusCodeExpected
123 })
124}
125
126function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
127 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
128
129 return makeDeleteRequest({
130 url,
131 path,
132 token,
133 statusCodeExpected
134 })
135}
136
137function getServerBlocklistByServer (
138 url: string,
139 token: string,
140 start: number,
141 count: number,
142 sort = '-createdAt',
143 statusCodeExpected = 200
144) {
145 const path = '/api/v1/server/blocklist/servers'
146
147 return makeGetRequest({
148 url,
149 token,
150 query: { start, count, sort },
151 path,
152 statusCodeExpected
153 })
154}
155
156function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
157 const path = '/api/v1/server/blocklist/servers'
158
159 return makePostBodyRequest({
160 url,
161 path,
162 token,
163 fields: {
164 host: serverToBlock
165 },
166 statusCodeExpected
167 })
168}
169
170function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
171 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
172
173 return makeDeleteRequest({
174 url,
175 path,
176 token,
177 statusCodeExpected
178 })
179}
180
181// ---------------------------------------------------------------------------
182
183export {
184 getAccountBlocklistByAccount,
185 addAccountToAccountBlocklist,
186 removeAccountFromAccountBlocklist,
187 getServerBlocklistByAccount,
188 addServerToAccountBlocklist,
189 removeServerFromAccountBlocklist,
190
191 getAccountBlocklistByServer,
192 addAccountToServerBlocklist,
193 removeAccountFromServerBlocklist,
194 getServerBlocklistByServer,
195 addServerToServerBlocklist,
196 removeServerFromServerBlocklist
197}
diff --git a/shared/utils/users/login.ts b/shared/utils/users/login.ts
new file mode 100644
index 000000000..ddeb9df2a
--- /dev/null
+++ b/shared/utils/users/login.ts
@@ -0,0 +1,62 @@
1import * as request from 'supertest'
2
3import { ServerInfo } from '../server/servers'
4
5type Client = { id: string, secret: string }
6type User = { username: string, password: string }
7type Server = { url: string, client: Client, user: User }
8
9function login (url: string, client: Client, user: User, expectedStatus = 200) {
10 const path = '/api/v1/users/token'
11
12 const body = {
13 client_id: client.id,
14 client_secret: client.secret,
15 username: user.username,
16 password: user.password,
17 response_type: 'code',
18 grant_type: 'password',
19 scope: 'upload'
20 }
21
22 return request(url)
23 .post(path)
24 .type('form')
25 .send(body)
26 .expect(expectedStatus)
27}
28
29async function serverLogin (server: Server) {
30 const res = await login(server.url, server.client, server.user, 200)
31
32 return res.body.access_token as string
33}
34
35async function userLogin (server: Server, user: User, expectedStatus = 200) {
36 const res = await login(server.url, server.client, user, expectedStatus)
37
38 return res.body.access_token as string
39}
40
41function setAccessTokensToServers (servers: ServerInfo[]) {
42 const tasks: Promise<any>[] = []
43
44 for (const server of servers) {
45 const p = serverLogin(server).then(t => server.accessToken = t)
46 tasks.push(p)
47 }
48
49 return Promise.all(tasks)
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 login,
56 serverLogin,
57 userLogin,
58 setAccessTokensToServers,
59 Server,
60 Client,
61 User
62}
diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts
new file mode 100644
index 000000000..c8ed7df30
--- /dev/null
+++ b/shared/utils/users/user-notifications.ts
@@ -0,0 +1,437 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
4import { UserNotification, UserNotificationSetting, UserNotificationType } from '../../models/users'
5import { ServerInfo } from '..'
6import { expect } from 'chai'
7import { inspect } from 'util'
8
9function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return makePutBodyRequest({
13 url,
14 path,
15 token,
16 fields: settings,
17 statusCodeExpected
18 })
19}
20
21function getUserNotifications (
22 url: string,
23 token: string,
24 start: number,
25 count: number,
26 unread?: boolean,
27 sort = '-createdAt',
28 statusCodeExpected = 200
29) {
30 const path = '/api/v1/users/me/notifications'
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query: {
37 start,
38 count,
39 sort,
40 unread
41 },
42 statusCodeExpected
43 })
44}
45
46function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = 204) {
47 const path = '/api/v1/users/me/notifications/read'
48
49 return makePostBodyRequest({
50 url,
51 path,
52 token,
53 fields: { ids },
54 statusCodeExpected
55 })
56}
57function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = 204) {
58 const path = '/api/v1/users/me/notifications/read-all'
59
60 return makePostBodyRequest({
61 url,
62 path,
63 token,
64 statusCodeExpected
65 })
66}
67
68async function getLastNotification (serverUrl: string, accessToken: string) {
69 const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
70
71 if (res.body.total === 0) return undefined
72
73 return res.body.data[0] as UserNotification
74}
75
76type CheckerBaseParams = {
77 server: ServerInfo
78 emails: object[]
79 socketNotifications: UserNotification[]
80 token: string,
81 check?: { web: boolean, mail: boolean }
82}
83
84type CheckerType = 'presence' | 'absence'
85
86async function checkNotification (
87 base: CheckerBaseParams,
88 notificationChecker: (notification: UserNotification, type: CheckerType) => void,
89 emailNotificationFinder: (email: object) => boolean,
90 checkType: CheckerType
91) {
92 const check = base.check || { web: true, mail: true }
93
94 if (check.web) {
95 const notification = await getLastNotification(base.server.url, base.token)
96
97 if (notification || checkType !== 'absence') {
98 notificationChecker(notification, checkType)
99 }
100
101 const socketNotification = base.socketNotifications.find(n => {
102 try {
103 notificationChecker(n, 'presence')
104 return true
105 } catch {
106 return false
107 }
108 })
109
110 if (checkType === 'presence') {
111 const obj = inspect(base.socketNotifications, { depth: 5 })
112 expect(socketNotification, 'The socket notification is absent. ' + obj).to.not.be.undefined
113 } else {
114 const obj = inspect(socketNotification, { depth: 5 })
115 expect(socketNotification, 'The socket notification is present. ' + obj).to.be.undefined
116 }
117 }
118
119 if (check.mail) {
120 // Last email
121 const email = base.emails
122 .slice()
123 .reverse()
124 .find(e => emailNotificationFinder(e))
125
126 if (checkType === 'presence') {
127 expect(email, 'The email is absent. ' + inspect(base.emails)).to.not.be.undefined
128 } else {
129 expect(email, 'The email is present. ' + inspect(email)).to.be.undefined
130 }
131 }
132}
133
134function checkVideo (video: any, videoName?: string, videoUUID?: string) {
135 expect(video.name).to.be.a('string')
136 expect(video.name).to.not.be.empty
137 if (videoName) expect(video.name).to.equal(videoName)
138
139 expect(video.uuid).to.be.a('string')
140 expect(video.uuid).to.not.be.empty
141 if (videoUUID) expect(video.uuid).to.equal(videoUUID)
142
143 expect(video.id).to.be.a('number')
144}
145
146function checkActor (actor: any) {
147 expect(actor.displayName).to.be.a('string')
148 expect(actor.displayName).to.not.be.empty
149 expect(actor.host).to.not.be.undefined
150}
151
152function checkComment (comment: any, commentId: number, threadId: number) {
153 expect(comment.id).to.equal(commentId)
154 expect(comment.threadId).to.equal(threadId)
155}
156
157async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
158 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
159
160 function notificationChecker (notification: UserNotification, type: CheckerType) {
161 if (type === 'presence') {
162 expect(notification).to.not.be.undefined
163 expect(notification.type).to.equal(notificationType)
164
165 checkVideo(notification.video, videoName, videoUUID)
166 checkActor(notification.video.channel)
167 } else {
168 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
169 }
170 }
171
172 function emailFinder (email: object) {
173 return email[ 'text' ].indexOf(videoUUID) !== -1
174 }
175
176 await checkNotification(base, notificationChecker, emailFinder, type)
177}
178
179async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
180 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
181
182 function notificationChecker (notification: UserNotification, type: CheckerType) {
183 if (type === 'presence') {
184 expect(notification).to.not.be.undefined
185 expect(notification.type).to.equal(notificationType)
186
187 checkVideo(notification.video, videoName, videoUUID)
188 checkActor(notification.video.channel)
189 } else {
190 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
191 }
192 }
193
194 function emailFinder (email: object) {
195 const text: string = email[ 'text' ]
196 return text.includes(videoUUID) && text.includes('Your video')
197 }
198
199 await checkNotification(base, notificationChecker, emailFinder, type)
200}
201
202async function checkMyVideoImportIsFinished (
203 base: CheckerBaseParams,
204 videoName: string,
205 videoUUID: string,
206 url: string,
207 success: boolean,
208 type: CheckerType
209) {
210 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
211
212 function notificationChecker (notification: UserNotification, type: CheckerType) {
213 if (type === 'presence') {
214 expect(notification).to.not.be.undefined
215 expect(notification.type).to.equal(notificationType)
216
217 expect(notification.videoImport.targetUrl).to.equal(url)
218
219 if (success) checkVideo(notification.videoImport.video, videoName, videoUUID)
220 } else {
221 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
222 }
223 }
224
225 function emailFinder (email: object) {
226 const text: string = email[ 'text' ]
227 const toFind = success ? ' finished' : ' error'
228
229 return text.includes(url) && text.includes(toFind)
230 }
231
232 await checkNotification(base, notificationChecker, emailFinder, type)
233}
234
235async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
236 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
237
238 function notificationChecker (notification: UserNotification, type: CheckerType) {
239 if (type === 'presence') {
240 expect(notification).to.not.be.undefined
241 expect(notification.type).to.equal(notificationType)
242
243 checkActor(notification.account)
244 expect(notification.account.name).to.equal(username)
245 } else {
246 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
247 }
248 }
249
250 function emailFinder (email: object) {
251 const text: string = email[ 'text' ]
252
253 return text.includes(' registered ') && text.includes(username)
254 }
255
256 await checkNotification(base, notificationChecker, emailFinder, type)
257}
258
259async function checkNewActorFollow (
260 base: CheckerBaseParams,
261 followType: 'channel' | 'account',
262 followerName: string,
263 followerDisplayName: string,
264 followingDisplayName: string,
265 type: CheckerType
266) {
267 const notificationType = UserNotificationType.NEW_FOLLOW
268
269 function notificationChecker (notification: UserNotification, type: CheckerType) {
270 if (type === 'presence') {
271 expect(notification).to.not.be.undefined
272 expect(notification.type).to.equal(notificationType)
273
274 checkActor(notification.actorFollow.follower)
275 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
276 expect(notification.actorFollow.follower.name).to.equal(followerName)
277 expect(notification.actorFollow.follower.host).to.not.be.undefined
278
279 expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
280 expect(notification.actorFollow.following.type).to.equal(followType)
281 } else {
282 expect(notification).to.satisfy(n => {
283 return n.type !== notificationType ||
284 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
285 })
286 }
287 }
288
289 function emailFinder (email: object) {
290 const text: string = email[ 'text' ]
291
292 return text.includes('Your ' + followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
293 }
294
295 await checkNotification(base, notificationChecker, emailFinder, type)
296}
297
298async function checkCommentMention (
299 base: CheckerBaseParams,
300 uuid: string,
301 commentId: number,
302 threadId: number,
303 byAccountDisplayName: string,
304 type: CheckerType
305) {
306 const notificationType = UserNotificationType.COMMENT_MENTION
307
308 function notificationChecker (notification: UserNotification, type: CheckerType) {
309 if (type === 'presence') {
310 expect(notification).to.not.be.undefined
311 expect(notification.type).to.equal(notificationType)
312
313 checkComment(notification.comment, commentId, threadId)
314 checkActor(notification.comment.account)
315 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
316
317 checkVideo(notification.comment.video, undefined, uuid)
318 } else {
319 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
320 }
321 }
322
323 function emailFinder (email: object) {
324 const text: string = email[ 'text' ]
325
326 return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
327 }
328
329 await checkNotification(base, notificationChecker, emailFinder, type)
330}
331
332let lastEmailCount = 0
333async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
334 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
335
336 function notificationChecker (notification: UserNotification, type: CheckerType) {
337 if (type === 'presence') {
338 expect(notification).to.not.be.undefined
339 expect(notification.type).to.equal(notificationType)
340
341 checkComment(notification.comment, commentId, threadId)
342 checkActor(notification.comment.account)
343 checkVideo(notification.comment.video, undefined, uuid)
344 } else {
345 expect(notification).to.satisfy((n: UserNotification) => {
346 return n === undefined || n.comment === undefined || n.comment.id !== commentId
347 })
348 }
349 }
350
351 const commentUrl = `http://localhost:9001/videos/watch/${uuid};threadId=${threadId}`
352 function emailFinder (email: object) {
353 return email[ 'text' ].indexOf(commentUrl) !== -1
354 }
355
356 await checkNotification(base, notificationChecker, emailFinder, type)
357
358 if (type === 'presence') {
359 // We cannot detect email duplicates, so check we received another email
360 expect(base.emails).to.have.length.above(lastEmailCount)
361 lastEmailCount = base.emails.length
362 }
363}
364
365async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
366 const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS
367
368 function notificationChecker (notification: UserNotification, type: CheckerType) {
369 if (type === 'presence') {
370 expect(notification).to.not.be.undefined
371 expect(notification.type).to.equal(notificationType)
372
373 expect(notification.videoAbuse.id).to.be.a('number')
374 checkVideo(notification.videoAbuse.video, videoName, videoUUID)
375 } else {
376 expect(notification).to.satisfy((n: UserNotification) => {
377 return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
378 })
379 }
380 }
381
382 function emailFinder (email: object) {
383 const text = email[ 'text' ]
384 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
385 }
386
387 await checkNotification(base, notificationChecker, emailFinder, type)
388}
389
390async function checkNewBlacklistOnMyVideo (
391 base: CheckerBaseParams,
392 videoUUID: string,
393 videoName: string,
394 blacklistType: 'blacklist' | 'unblacklist'
395) {
396 const notificationType = blacklistType === 'blacklist'
397 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
398 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
399
400 function notificationChecker (notification: UserNotification) {
401 expect(notification).to.not.be.undefined
402 expect(notification.type).to.equal(notificationType)
403
404 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
405
406 checkVideo(video, videoName, videoUUID)
407 }
408
409 function emailFinder (email: object) {
410 const text = email[ 'text' ]
411 return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
412 }
413
414 await checkNotification(base, notificationChecker, emailFinder, 'presence')
415}
416
417// ---------------------------------------------------------------------------
418
419export {
420 CheckerBaseParams,
421 CheckerType,
422 checkNotification,
423 markAsReadAllNotifications,
424 checkMyVideoImportIsFinished,
425 checkUserRegistered,
426 checkVideoIsPublished,
427 checkNewVideoFromSubscription,
428 checkNewActorFollow,
429 checkNewCommentOnMyVideo,
430 checkNewBlacklistOnMyVideo,
431 checkCommentMention,
432 updateMyNotificationSettings,
433 checkNewVideoAbuseForModerators,
434 getUserNotifications,
435 markAsReadNotifications,
436 getLastNotification
437}
diff --git a/shared/utils/users/user-subscriptions.ts b/shared/utils/users/user-subscriptions.ts
new file mode 100644
index 000000000..7148fbfca
--- /dev/null
+++ b/shared/utils/users/user-subscriptions.ts
@@ -0,0 +1,82 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2
3function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = 204) {
4 const path = '/api/v1/users/me/subscriptions'
5
6 return makePostBodyRequest({
7 url,
8 path,
9 token,
10 statusCodeExpected,
11 fields: { uri: targetUri }
12 })
13}
14
15function listUserSubscriptions (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
16 const path = '/api/v1/users/me/subscriptions'
17
18 return makeGetRequest({
19 url,
20 path,
21 token,
22 statusCodeExpected,
23 query: { sort }
24 })
25}
26
27function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
28 const path = '/api/v1/users/me/subscriptions/videos'
29
30 return makeGetRequest({
31 url,
32 path,
33 token,
34 statusCodeExpected,
35 query: { sort }
36 })
37}
38
39function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 200) {
40 const path = '/api/v1/users/me/subscriptions/' + uri
41
42 return makeGetRequest({
43 url,
44 path,
45 token,
46 statusCodeExpected
47 })
48}
49
50function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) {
51 const path = '/api/v1/users/me/subscriptions/' + uri
52
53 return makeDeleteRequest({
54 url,
55 path,
56 token,
57 statusCodeExpected
58 })
59}
60
61function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) {
62 const path = '/api/v1/users/me/subscriptions/exist'
63
64 return makeGetRequest({
65 url,
66 path,
67 query: { 'uris[]': uris },
68 token,
69 statusCodeExpected
70 })
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 areSubscriptionsExist,
77 addUserSubscription,
78 listUserSubscriptions,
79 getUserSubscription,
80 listUserSubscriptionVideos,
81 removeUserSubscription
82}
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts
new file mode 100644
index 000000000..7191b263e
--- /dev/null
+++ b/shared/utils/users/users.ts
@@ -0,0 +1,304 @@
1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3
4import { UserRole } from '../../index'
5import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
6
7function createUser (
8 url: string,
9 accessToken: string,
10 username: string,
11 password: string,
12 videoQuota = 1000000,
13 videoQuotaDaily = -1,
14 role: UserRole = UserRole.USER,
15 specialStatus = 200
16) {
17 const path = '/api/v1/users'
18 const body = {
19 username,
20 password,
21 role,
22 email: username + '@example.com',
23 videoQuota,
24 videoQuotaDaily
25 }
26
27 return request(url)
28 .post(path)
29 .set('Accept', 'application/json')
30 .set('Authorization', 'Bearer ' + accessToken)
31 .send(body)
32 .expect(specialStatus)
33}
34
35function registerUser (url: string, username: string, password: string, specialStatus = 204) {
36 const path = '/api/v1/users/register'
37 const body = {
38 username,
39 password,
40 email: username + '@example.com'
41 }
42
43 return request(url)
44 .post(path)
45 .set('Accept', 'application/json')
46 .send(body)
47 .expect(specialStatus)
48}
49
50function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) {
51 const path = '/api/v1/users/me'
52
53 return request(url)
54 .get(path)
55 .set('Accept', 'application/json')
56 .set('Authorization', 'Bearer ' + accessToken)
57 .expect(specialStatus)
58 .expect('Content-Type', /json/)
59}
60
61function deleteMe (url: string, accessToken: string, specialStatus = 204) {
62 const path = '/api/v1/users/me'
63
64 return request(url)
65 .delete(path)
66 .set('Accept', 'application/json')
67 .set('Authorization', 'Bearer ' + accessToken)
68 .expect(specialStatus)
69}
70
71function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
72 const path = '/api/v1/users/me/video-quota-used'
73
74 return request(url)
75 .get(path)
76 .set('Accept', 'application/json')
77 .set('Authorization', 'Bearer ' + accessToken)
78 .expect(specialStatus)
79 .expect('Content-Type', /json/)
80}
81
82function getUserInformation (url: string, accessToken: string, userId: number) {
83 const path = '/api/v1/users/' + userId
84
85 return request(url)
86 .get(path)
87 .set('Accept', 'application/json')
88 .set('Authorization', 'Bearer ' + accessToken)
89 .expect(200)
90 .expect('Content-Type', /json/)
91}
92
93function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = 200) {
94 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
95
96 return request(url)
97 .get(path)
98 .set('Accept', 'application/json')
99 .set('Authorization', 'Bearer ' + accessToken)
100 .expect(specialStatus)
101 .expect('Content-Type', /json/)
102}
103
104function getUsersList (url: string, accessToken: string) {
105 const path = '/api/v1/users'
106
107 return request(url)
108 .get(path)
109 .set('Accept', 'application/json')
110 .set('Authorization', 'Bearer ' + accessToken)
111 .expect(200)
112 .expect('Content-Type', /json/)
113}
114
115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) {
116 const path = '/api/v1/users'
117
118 return request(url)
119 .get(path)
120 .query({ start })
121 .query({ count })
122 .query({ sort })
123 .query({ search })
124 .set('Accept', 'application/json')
125 .set('Authorization', 'Bearer ' + accessToken)
126 .expect(200)
127 .expect('Content-Type', /json/)
128}
129
130function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
131 const path = '/api/v1/users'
132
133 return request(url)
134 .delete(path + '/' + userId)
135 .set('Accept', 'application/json')
136 .set('Authorization', 'Bearer ' + accessToken)
137 .expect(expectedStatus)
138}
139
140function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
141 const path = '/api/v1/users'
142 let body: any
143 if (reason) body = { reason }
144
145 return request(url)
146 .post(path + '/' + userId + '/block')
147 .send(body)
148 .set('Accept', 'application/json')
149 .set('Authorization', 'Bearer ' + accessToken)
150 .expect(expectedStatus)
151}
152
153function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
154 const path = '/api/v1/users'
155
156 return request(url)
157 .post(path + '/' + userId + '/unblock')
158 .set('Accept', 'application/json')
159 .set('Authorization', 'Bearer ' + accessToken)
160 .expect(expectedStatus)
161}
162
163function updateMyUser (options: {
164 url: string
165 accessToken: string
166 currentPassword?: string
167 newPassword?: string
168 nsfwPolicy?: NSFWPolicyType
169 email?: string
170 autoPlayVideo?: boolean
171 displayName?: string
172 description?: string
173 videosHistoryEnabled?: boolean
174}) {
175 const path = '/api/v1/users/me'
176
177 const toSend = {}
178 if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword
179 if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword
180 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy
181 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
182 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
183 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
184 if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
185 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
186 toSend['videosHistoryEnabled'] = options.videosHistoryEnabled
187 }
188
189 return makePutBodyRequest({
190 url: options.url,
191 path,
192 token: options.accessToken,
193 fields: toSend,
194 statusCodeExpected: 204
195 })
196}
197
198function updateMyAvatar (options: {
199 url: string,
200 accessToken: string,
201 fixture: string
202}) {
203 const path = '/api/v1/users/me/avatar/pick'
204
205 return updateAvatarRequest(Object.assign(options, { path }))
206}
207
208function updateUser (options: {
209 url: string
210 userId: number,
211 accessToken: string,
212 email?: string,
213 emailVerified?: boolean,
214 videoQuota?: number,
215 videoQuotaDaily?: number,
216 password?: string,
217 role?: UserRole
218}) {
219 const path = '/api/v1/users/' + options.userId
220
221 const toSend = {}
222 if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
223 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
224 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
225 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
226 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
227 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
228
229 return makePutBodyRequest({
230 url: options.url,
231 path,
232 token: options.accessToken,
233 fields: toSend,
234 statusCodeExpected: 204
235 })
236}
237
238function askResetPassword (url: string, email: string) {
239 const path = '/api/v1/users/ask-reset-password'
240
241 return makePostBodyRequest({
242 url,
243 path,
244 fields: { email },
245 statusCodeExpected: 204
246 })
247}
248
249function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) {
250 const path = '/api/v1/users/' + userId + '/reset-password'
251
252 return makePostBodyRequest({
253 url,
254 path,
255 fields: { password, verificationString },
256 statusCodeExpected
257 })
258}
259
260function askSendVerifyEmail (url: string, email: string) {
261 const path = '/api/v1/users/ask-send-verify-email'
262
263 return makePostBodyRequest({
264 url,
265 path,
266 fields: { email },
267 statusCodeExpected: 204
268 })
269}
270
271function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
272 const path = '/api/v1/users/' + userId + '/verify-email'
273
274 return makePostBodyRequest({
275 url,
276 path,
277 fields: { verificationString },
278 statusCodeExpected
279 })
280}
281
282// ---------------------------------------------------------------------------
283
284export {
285 createUser,
286 registerUser,
287 getMyUserInformation,
288 getMyUserVideoRating,
289 deleteMe,
290 getMyUserVideoQuotaUsed,
291 getUsersList,
292 getUsersListPaginationAndSort,
293 removeUser,
294 updateUser,
295 updateMyUser,
296 getUserInformation,
297 blockUser,
298 unblockUser,
299 askResetPassword,
300 resetPassword,
301 updateMyAvatar,
302 askSendVerifyEmail,
303 verifyEmail
304}
diff --git a/shared/utils/videos/services.ts b/shared/utils/videos/services.ts
new file mode 100644
index 000000000..1a53dd4cf
--- /dev/null
+++ b/shared/utils/videos/services.ts
@@ -0,0 +1,23 @@
1import * as request from 'supertest'
2
3function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
4 const path = '/services/oembed'
5 const query = {
6 url: oembedUrl,
7 format,
8 maxheight: maxHeight,
9 maxwidth: maxWidth
10 }
11
12 return request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16 .expect(200)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getOEmbed
23}
diff --git a/shared/utils/videos/video-abuses.ts b/shared/utils/videos/video-abuses.ts
new file mode 100644
index 000000000..7f011ec0f
--- /dev/null
+++ b/shared/utils/videos/video-abuses.ts
@@ -0,0 +1,65 @@
1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '../requests/requests'
4
5function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
6 const path = '/api/v1/videos/' + videoId + '/abuse'
7
8 return request(url)
9 .post(path)
10 .set('Accept', 'application/json')
11 .set('Authorization', 'Bearer ' + token)
12 .send({ reason })
13 .expect(specialStatus)
14}
15
16function getVideoAbusesList (url: string, token: string) {
17 const path = '/api/v1/videos/abuse'
18
19 return request(url)
20 .get(path)
21 .query({ sort: 'createdAt' })
22 .set('Accept', 'application/json')
23 .set('Authorization', 'Bearer ' + token)
24 .expect(200)
25 .expect('Content-Type', /json/)
26}
27
28function updateVideoAbuse (
29 url: string,
30 token: string,
31 videoId: string | number,
32 videoAbuseId: number,
33 body: VideoAbuseUpdate,
34 statusCodeExpected = 204
35) {
36 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: body,
43 statusCodeExpected
44 })
45}
46
47function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
48 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
49
50 return makeDeleteRequest({
51 url,
52 token,
53 path,
54 statusCodeExpected
55 })
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 reportVideoAbuse,
62 getVideoAbusesList,
63 updateVideoAbuse,
64 deleteVideoAbuse
65}
diff --git a/shared/utils/videos/video-blacklist.ts b/shared/utils/videos/video-blacklist.ts
new file mode 100644
index 000000000..f2ae0ed26
--- /dev/null
+++ b/shared/utils/videos/video-blacklist.ts
@@ -0,0 +1,74 @@
1import * as request from 'supertest'
2
3function addVideoToBlacklist (
4 url: string,
5 token: string,
6 videoId: number | string,
7 reason?: string,
8 unfederate?: boolean,
9 specialStatus = 204
10) {
11 const path = '/api/v1/videos/' + videoId + '/blacklist'
12
13 return request(url)
14 .post(path)
15 .send({ reason, unfederate })
16 .set('Accept', 'application/json')
17 .set('Authorization', 'Bearer ' + token)
18 .expect(specialStatus)
19}
20
21function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) {
22 const path = '/api/v1/videos/' + videoId + '/blacklist'
23
24 return request(url)
25 .put(path)
26 .send({ reason })
27 .set('Accept', 'application/json')
28 .set('Authorization', 'Bearer ' + token)
29 .expect(specialStatus)
30}
31
32function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
33 const path = '/api/v1/videos/' + videoId + '/blacklist'
34
35 return request(url)
36 .delete(path)
37 .set('Accept', 'application/json')
38 .set('Authorization', 'Bearer ' + token)
39 .expect(specialStatus)
40}
41
42function getBlacklistedVideosList (url: string, token: string, specialStatus = 200) {
43 const path = '/api/v1/videos/blacklist/'
44
45 return request(url)
46 .get(path)
47 .query({ sort: 'createdAt' })
48 .set('Accept', 'application/json')
49 .set('Authorization', 'Bearer ' + token)
50 .expect(specialStatus)
51 .expect('Content-Type', /json/)
52}
53
54function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
55 const path = '/api/v1/videos/blacklist/'
56
57 return request(url)
58 .get(path)
59 .query({ sort: sort })
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(specialStatus)
63 .expect('Content-Type', /json/)
64}
65
66// ---------------------------------------------------------------------------
67
68export {
69 addVideoToBlacklist,
70 removeVideoFromBlacklist,
71 getBlacklistedVideosList,
72 getSortedBlacklistedVideosList,
73 updateVideoBlacklist
74}
diff --git a/shared/utils/videos/video-captions.ts b/shared/utils/videos/video-captions.ts
new file mode 100644
index 000000000..8d67f617b
--- /dev/null
+++ b/shared/utils/videos/video-captions.ts
@@ -0,0 +1,71 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5
6const expect = chai.expect
7
8function createVideoCaption (args: {
9 url: string,
10 accessToken: string
11 videoId: string | number
12 language: string
13 fixture: string,
14 mimeType?: string,
15 statusCodeExpected?: number
16}) {
17 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
18
19 const captionfile = buildAbsoluteFixturePath(args.fixture)
20 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
21
22 return makeUploadRequest({
23 method: 'PUT',
24 url: args.url,
25 path,
26 token: args.accessToken,
27 fields: {},
28 attaches: {
29 captionfile: captionfileAttach
30 },
31 statusCodeExpected: args.statusCodeExpected || 204
32 })
33}
34
35function listVideoCaptions (url: string, videoId: string | number) {
36 const path = '/api/v1/videos/' + videoId + '/captions'
37
38 return makeGetRequest({
39 url,
40 path,
41 statusCodeExpected: 200
42 })
43}
44
45function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
46 const path = '/api/v1/videos/' + videoId + '/captions/' + language
47
48 return makeDeleteRequest({
49 url,
50 token,
51 path,
52 statusCodeExpected: 204
53 })
54}
55
56async function testCaptionFile (url: string, captionPath: string, containsString: string) {
57 const res = await request(url)
58 .get(captionPath)
59 .expect(200)
60
61 expect(res.text).to.contain(containsString)
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 createVideoCaption,
68 listVideoCaptions,
69 testCaptionFile,
70 deleteVideoCaption
71}
diff --git a/shared/utils/videos/video-change-ownership.ts b/shared/utils/videos/video-change-ownership.ts
new file mode 100644
index 000000000..f288692ea
--- /dev/null
+++ b/shared/utils/videos/video-change-ownership.ts
@@ -0,0 +1,54 @@
1import * as request from 'supertest'
2
3function changeVideoOwnership (url: string, token: string, videoId: number | string, username) {
4 const path = '/api/v1/videos/' + videoId + '/give-ownership'
5
6 return request(url)
7 .post(path)
8 .set('Accept', 'application/json')
9 .set('Authorization', 'Bearer ' + token)
10 .send({ username })
11 .expect(204)
12}
13
14function getVideoChangeOwnershipList (url: string, token: string) {
15 const path = '/api/v1/videos/ownership'
16
17 return request(url)
18 .get(path)
19 .query({ sort: '-createdAt' })
20 .set('Accept', 'application/json')
21 .set('Authorization', 'Bearer ' + token)
22 .expect(200)
23 .expect('Content-Type', /json/)
24}
25
26function acceptChangeOwnership (url: string, token: string, ownershipId: string, channelId: number, expectedStatus = 204) {
27 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
28
29 return request(url)
30 .post(path)
31 .set('Accept', 'application/json')
32 .set('Authorization', 'Bearer ' + token)
33 .send({ channelId })
34 .expect(expectedStatus)
35}
36
37function refuseChangeOwnership (url: string, token: string, ownershipId: string, expectedStatus = 204) {
38 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
39
40 return request(url)
41 .post(path)
42 .set('Accept', 'application/json')
43 .set('Authorization', 'Bearer ' + token)
44 .expect(expectedStatus)
45}
46
47// ---------------------------------------------------------------------------
48
49export {
50 changeVideoOwnership,
51 getVideoChangeOwnershipList,
52 acceptChangeOwnership,
53 refuseChangeOwnership
54}
diff --git a/shared/utils/videos/video-channels.ts b/shared/utils/videos/video-channels.ts
new file mode 100644
index 000000000..3935c261e
--- /dev/null
+++ b/shared/utils/videos/video-channels.ts
@@ -0,0 +1,118 @@
1import * as request from 'supertest'
2import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
3import { updateAvatarRequest } from '../requests/requests'
4
5function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
6 const path = '/api/v1/video-channels'
7
8 const req = request(url)
9 .get(path)
10 .query({ start: start })
11 .query({ count: count })
12
13 if (sort) req.query({ sort })
14
15 return req.set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function getAccountVideoChannelsList (url: string, accountName: string, specialStatus = 200) {
21 const path = '/api/v1/accounts/' + accountName + '/video-channels'
22
23 return request(url)
24 .get(path)
25 .set('Accept', 'application/json')
26 .expect(specialStatus)
27 .expect('Content-Type', /json/)
28}
29
30function addVideoChannel (
31 url: string,
32 token: string,
33 videoChannelAttributesArg: VideoChannelCreate,
34 expectedStatus = 200
35) {
36 const path = '/api/v1/video-channels/'
37
38 // Default attributes
39 let attributes = {
40 displayName: 'my super video channel',
41 description: 'my super channel description',
42 support: 'my super channel support'
43 }
44 attributes = Object.assign(attributes, videoChannelAttributesArg)
45
46 return request(url)
47 .post(path)
48 .send(attributes)
49 .set('Accept', 'application/json')
50 .set('Authorization', 'Bearer ' + token)
51 .expect(expectedStatus)
52}
53
54function updateVideoChannel (
55 url: string,
56 token: string,
57 channelName: string,
58 attributes: VideoChannelUpdate,
59 expectedStatus = 204
60) {
61 const body = {}
62 const path = '/api/v1/video-channels/' + channelName
63
64 if (attributes.displayName) body['displayName'] = attributes.displayName
65 if (attributes.description) body['description'] = attributes.description
66 if (attributes.support) body['support'] = attributes.support
67
68 return request(url)
69 .put(path)
70 .send(body)
71 .set('Accept', 'application/json')
72 .set('Authorization', 'Bearer ' + token)
73 .expect(expectedStatus)
74}
75
76function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = 204) {
77 const path = '/api/v1/video-channels/' + channelName
78
79 return request(url)
80 .delete(path)
81 .set('Accept', 'application/json')
82 .set('Authorization', 'Bearer ' + token)
83 .expect(expectedStatus)
84}
85
86function getVideoChannel (url: string, channelName: string) {
87 const path = '/api/v1/video-channels/' + channelName
88
89 return request(url)
90 .get(path)
91 .set('Accept', 'application/json')
92 .expect(200)
93 .expect('Content-Type', /json/)
94}
95
96function updateVideoChannelAvatar (options: {
97 url: string,
98 accessToken: string,
99 fixture: string,
100 videoChannelName: string | number
101}) {
102
103 const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick'
104
105 return updateAvatarRequest(Object.assign(options, { path }))
106}
107
108// ---------------------------------------------------------------------------
109
110export {
111 updateVideoChannelAvatar,
112 getVideoChannelsList,
113 getAccountVideoChannelsList,
114 addVideoChannel,
115 updateVideoChannel,
116 deleteVideoChannel,
117 getVideoChannel
118}
diff --git a/shared/utils/videos/video-comments.ts b/shared/utils/videos/video-comments.ts
new file mode 100644
index 000000000..0ebf69ced
--- /dev/null
+++ b/shared/utils/videos/video-comments.ts
@@ -0,0 +1,87 @@
1import * as request from 'supertest'
2import { makeDeleteRequest } from '../requests/requests'
3
4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
5 const path = '/api/v1/videos/' + videoId + '/comment-threads'
6
7 const req = request(url)
8 .get(path)
9 .query({ start: start })
10 .query({ count: count })
11
12 if (sort) req.query({ sort })
13 if (token) req.set('Authorization', 'Bearer ' + token)
14
15 return req.set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
21 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
22
23 const req = request(url)
24 .get(path)
25 .set('Accept', 'application/json')
26
27 if (token) req.set('Authorization', 'Bearer ' + token)
28
29 return req.expect(200)
30 .expect('Content-Type', /json/)
31}
32
33function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
34 const path = '/api/v1/videos/' + videoId + '/comment-threads'
35
36 return request(url)
37 .post(path)
38 .send({ text })
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + token)
41 .expect(expectedStatus)
42}
43
44function addVideoCommentReply (
45 url: string,
46 token: string,
47 videoId: number | string,
48 inReplyToCommentId: number,
49 text: string,
50 expectedStatus = 200
51) {
52 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
53
54 return request(url)
55 .post(path)
56 .send({ text })
57 .set('Accept', 'application/json')
58 .set('Authorization', 'Bearer ' + token)
59 .expect(expectedStatus)
60}
61
62function deleteVideoComment (
63 url: string,
64 token: string,
65 videoId: number | string,
66 commentId: number,
67 statusCodeExpected = 204
68) {
69 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
70
71 return makeDeleteRequest({
72 url,
73 path,
74 token,
75 statusCodeExpected
76 })
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 getVideoCommentThreads,
83 getVideoThreadComments,
84 addVideoCommentThread,
85 addVideoCommentReply,
86 deleteVideoComment
87}
diff --git a/shared/utils/videos/video-history.ts b/shared/utils/videos/video-history.ts
new file mode 100644
index 000000000..dc7095b4d
--- /dev/null
+++ b/shared/utils/videos/video-history.ts
@@ -0,0 +1,39 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number, statusCodeExpected = 204) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
8}
9
10function listMyVideosHistory (url: string, token: string) {
11 const path = '/api/v1/users/me/history/videos'
12
13 return makeGetRequest({
14 url,
15 path,
16 token,
17 statusCodeExpected: 200
18 })
19}
20
21function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
22 const path = '/api/v1/users/me/history/videos/remove'
23
24 return makePostBodyRequest({
25 url,
26 path,
27 token,
28 fields: beforeDate ? { beforeDate } : {},
29 statusCodeExpected: 204
30 })
31}
32
33// ---------------------------------------------------------------------------
34
35export {
36 userWatchVideo,
37 listMyVideosHistory,
38 removeMyVideosHistory
39}
diff --git a/shared/utils/videos/video-imports.ts b/shared/utils/videos/video-imports.ts
new file mode 100644
index 000000000..ec77cdcda
--- /dev/null
+++ b/shared/utils/videos/video-imports.ts
@@ -0,0 +1,57 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4
5function getYoutubeVideoUrl () {
6 return 'https://youtu.be/msX3jv1XdvM'
7}
8
9function getMagnetURI () {
10 // tslint:disable:max-line-length
11 return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
12}
13
14function getBadVideoUrl () {
15 return 'https://download.cpy.re/peertube/bad_video.mp4'
16}
17
18function importVideo (url: string, token: string, attributes: VideoImportCreate) {
19 const path = '/api/v1/videos/imports'
20
21 let attaches: any = {}
22 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
23
24 return makeUploadRequest({
25 url,
26 path,
27 token,
28 attaches,
29 fields: attributes,
30 statusCodeExpected: 200
31 })
32}
33
34function getMyVideoImports (url: string, token: string, sort?: string) {
35 const path = '/api/v1/users/me/videos/imports'
36
37 const query = {}
38 if (sort) query['sort'] = sort
39
40 return makeGetRequest({
41 url,
42 query,
43 path,
44 token,
45 statusCodeExpected: 200
46 })
47}
48
49// ---------------------------------------------------------------------------
50
51export {
52 getBadVideoUrl,
53 getYoutubeVideoUrl,
54 importVideo,
55 getMagnetURI,
56 getMyVideoImports
57}
diff --git a/shared/utils/videos/video-playlists.ts b/shared/utils/videos/video-playlists.ts
new file mode 100644
index 000000000..eb25011cb
--- /dev/null
+++ b/shared/utils/videos/video-playlists.ts
@@ -0,0 +1,51 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5
6function getPlaylist (url: string, statusCodeExpected = 200) {
7 return makeRawRequest(url, statusCodeExpected)
8}
9
10function getSegment (url: string, statusCodeExpected = 200, range?: string) {
11 return makeRawRequest(url, statusCodeExpected, range)
12}
13
14function getSegmentSha256 (url: string, statusCodeExpected = 200) {
15 return makeRawRequest(url, statusCodeExpected)
16}
17
18async function checkSegmentHash (
19 baseUrlPlaylist: string,
20 baseUrlSegment: string,
21 videoUUID: string,
22 resolution: number,
23 hlsPlaylist: VideoStreamingPlaylist
24) {
25 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
26 const playlist = res.text
27
28 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
29
30 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
31
32 const length = parseInt(matches[1], 10)
33 const offset = parseInt(matches[2], 10)
34 const range = `${offset}-${offset + length - 1}`
35
36 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`)
37
38 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
39
40 const sha256Server = resSha.body[ videoName ][range]
41 expect(sha256(res2.body)).to.equal(sha256Server)
42}
43
44// ---------------------------------------------------------------------------
45
46export {
47 getPlaylist,
48 getSegment,
49 getSegmentSha256,
50 checkSegmentHash
51}
diff --git a/shared/utils/videos/videos.ts b/shared/utils/videos/videos.ts
new file mode 100644
index 000000000..39c808d1f
--- /dev/null
+++ b/shared/utils/videos/videos.ts
@@ -0,0 +1,592 @@
1/* tslint:disable:no-unused-expression */
2
3import { expect } from 'chai'
4import { existsSync, readdir, readFile } from 'fs-extra'
5import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path'
7import * as request from 'supertest'
8import {
9 buildAbsoluteFixturePath,
10 getMyUserInformation,
11 immutableAssign,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 root,
16 ServerInfo,
17 testImage
18} from '../'
19
20import { VideoDetails, VideoPrivacy } from '../../models/videos'
21import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
22import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
23
24type VideoAttributes = {
25 name?: string
26 category?: number
27 licence?: number
28 language?: string
29 nsfw?: boolean
30 commentsEnabled?: boolean
31 downloadEnabled?: boolean
32 waitTranscoding?: boolean
33 description?: string
34 tags?: string[]
35 channelId?: number
36 privacy?: VideoPrivacy
37 fixture?: string
38 thumbnailfile?: string
39 previewfile?: string
40 scheduleUpdate?: {
41 updateAt: string
42 privacy?: VideoPrivacy
43 }
44}
45
46function getVideoCategories (url: string) {
47 const path = '/api/v1/videos/categories'
48
49 return makeGetRequest({
50 url,
51 path,
52 statusCodeExpected: 200
53 })
54}
55
56function getVideoLicences (url: string) {
57 const path = '/api/v1/videos/licences'
58
59 return makeGetRequest({
60 url,
61 path,
62 statusCodeExpected: 200
63 })
64}
65
66function getVideoLanguages (url: string) {
67 const path = '/api/v1/videos/languages'
68
69 return makeGetRequest({
70 url,
71 path,
72 statusCodeExpected: 200
73 })
74}
75
76function getVideoPrivacies (url: string) {
77 const path = '/api/v1/videos/privacies'
78
79 return makeGetRequest({
80 url,
81 path,
82 statusCodeExpected: 200
83 })
84}
85
86function getVideo (url: string, id: number | string, expectedStatus = 200) {
87 const path = '/api/v1/videos/' + id
88
89 return request(url)
90 .get(path)
91 .set('Accept', 'application/json')
92 .expect(expectedStatus)
93}
94
95function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
96 const path = '/api/v1/videos/' + id + '/views'
97
98 const req = request(url)
99 .post(path)
100 .set('Accept', 'application/json')
101
102 if (xForwardedFor) {
103 req.set('X-Forwarded-For', xForwardedFor)
104 }
105
106 return req.expect(expectedStatus)
107}
108
109function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
110 const path = '/api/v1/videos/' + id
111
112 return request(url)
113 .get(path)
114 .set('Authorization', 'Bearer ' + token)
115 .set('Accept', 'application/json')
116 .expect(expectedStatus)
117}
118
119function getVideoDescription (url: string, descriptionPath: string) {
120 return request(url)
121 .get(descriptionPath)
122 .set('Accept', 'application/json')
123 .expect(200)
124 .expect('Content-Type', /json/)
125}
126
127function getVideosList (url: string) {
128 const path = '/api/v1/videos'
129
130 return request(url)
131 .get(path)
132 .query({ sort: 'name' })
133 .set('Accept', 'application/json')
134 .expect(200)
135 .expect('Content-Type', /json/)
136}
137
138function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
139 const path = '/api/v1/videos'
140
141 return request(url)
142 .get(path)
143 .set('Authorization', 'Bearer ' + token)
144 .query(immutableAssign(query, { sort: 'name' }))
145 .set('Accept', 'application/json')
146 .expect(200)
147 .expect('Content-Type', /json/)
148}
149
150function getLocalVideos (url: string) {
151 const path = '/api/v1/videos'
152
153 return request(url)
154 .get(path)
155 .query({ sort: 'name', filter: 'local' })
156 .set('Accept', 'application/json')
157 .expect(200)
158 .expect('Content-Type', /json/)
159}
160
161function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
162 const path = '/api/v1/users/me/videos'
163
164 const req = request(url)
165 .get(path)
166 .query({ start: start })
167 .query({ count: count })
168
169 if (sort) req.query({ sort })
170
171 return req.set('Accept', 'application/json')
172 .set('Authorization', 'Bearer ' + accessToken)
173 .expect(200)
174 .expect('Content-Type', /json/)
175}
176
177function getAccountVideos (
178 url: string,
179 accessToken: string,
180 accountName: string,
181 start: number,
182 count: number,
183 sort?: string,
184 query: { nsfw?: boolean } = {}
185) {
186 const path = '/api/v1/accounts/' + accountName + '/videos'
187
188 return makeGetRequest({
189 url,
190 path,
191 query: immutableAssign(query, {
192 start,
193 count,
194 sort
195 }),
196 token: accessToken,
197 statusCodeExpected: 200
198 })
199}
200
201function getVideoChannelVideos (
202 url: string,
203 accessToken: string,
204 videoChannelName: string,
205 start: number,
206 count: number,
207 sort?: string,
208 query: { nsfw?: boolean } = {}
209) {
210 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
211
212 return makeGetRequest({
213 url,
214 path,
215 query: immutableAssign(query, {
216 start,
217 count,
218 sort
219 }),
220 token: accessToken,
221 statusCodeExpected: 200
222 })
223}
224
225function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
226 const path = '/api/v1/videos'
227
228 const req = request(url)
229 .get(path)
230 .query({ start: start })
231 .query({ count: count })
232
233 if (sort) req.query({ sort })
234
235 return req.set('Accept', 'application/json')
236 .expect(200)
237 .expect('Content-Type', /json/)
238}
239
240function getVideosListSort (url: string, sort: string) {
241 const path = '/api/v1/videos'
242
243 return request(url)
244 .get(path)
245 .query({ sort: sort })
246 .set('Accept', 'application/json')
247 .expect(200)
248 .expect('Content-Type', /json/)
249}
250
251function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
252 const path = '/api/v1/videos'
253
254 return request(url)
255 .get(path)
256 .query(query)
257 .set('Accept', 'application/json')
258 .expect(200)
259 .expect('Content-Type', /json/)
260}
261
262function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
263 const path = '/api/v1/videos'
264
265 return request(url)
266 .delete(path + '/' + id)
267 .set('Accept', 'application/json')
268 .set('Authorization', 'Bearer ' + token)
269 .expect(expectedStatus)
270}
271
272async function checkVideoFilesWereRemoved (
273 videoUUID: string,
274 serverNumber: number,
275 directories = [
276 'redundancy',
277 'videos',
278 'thumbnails',
279 'torrents',
280 'previews',
281 'captions',
282 join('playlists', 'hls'),
283 join('redundancy', 'hls')
284 ]
285) {
286 const testDirectory = 'test' + serverNumber
287
288 for (const directory of directories) {
289 const directoryPath = join(root(), testDirectory, directory)
290
291 const directoryExists = existsSync(directoryPath)
292 if (!directoryExists) continue
293
294 const files = await readdir(directoryPath)
295 for (const file of files) {
296 expect(file).to.not.contain(videoUUID)
297 }
298 }
299}
300
301async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
302 const path = '/api/v1/videos/upload'
303 let defaultChannelId = '1'
304
305 try {
306 const res = await getMyUserInformation(url, accessToken)
307 defaultChannelId = res.body.videoChannels[0].id
308 } catch (e) { /* empty */ }
309
310 // Override default attributes
311 const attributes = Object.assign({
312 name: 'my super video',
313 category: 5,
314 licence: 4,
315 language: 'zh',
316 channelId: defaultChannelId,
317 nsfw: true,
318 waitTranscoding: false,
319 description: 'my super description',
320 support: 'my super support text',
321 tags: [ 'tag' ],
322 privacy: VideoPrivacy.PUBLIC,
323 commentsEnabled: true,
324 downloadEnabled: true,
325 fixture: 'video_short.webm'
326 }, videoAttributesArg)
327
328 const req = request(url)
329 .post(path)
330 .set('Accept', 'application/json')
331 .set('Authorization', 'Bearer ' + accessToken)
332 .field('name', attributes.name)
333 .field('nsfw', JSON.stringify(attributes.nsfw))
334 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
335 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
336 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
337 .field('privacy', attributes.privacy.toString())
338 .field('channelId', attributes.channelId)
339
340 if (attributes.description !== undefined) {
341 req.field('description', attributes.description)
342 }
343 if (attributes.language !== undefined) {
344 req.field('language', attributes.language.toString())
345 }
346 if (attributes.category !== undefined) {
347 req.field('category', attributes.category.toString())
348 }
349 if (attributes.licence !== undefined) {
350 req.field('licence', attributes.licence.toString())
351 }
352
353 for (let i = 0; i < attributes.tags.length; i++) {
354 req.field('tags[' + i + ']', attributes.tags[i])
355 }
356
357 if (attributes.thumbnailfile !== undefined) {
358 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
359 }
360 if (attributes.previewfile !== undefined) {
361 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
362 }
363
364 if (attributes.scheduleUpdate) {
365 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
366
367 if (attributes.scheduleUpdate.privacy) {
368 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
369 }
370 }
371
372 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
373 .expect(specialStatus)
374}
375
376function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
377 const path = '/api/v1/videos/' + id
378 const body = {}
379
380 if (attributes.name) body['name'] = attributes.name
381 if (attributes.category) body['category'] = attributes.category
382 if (attributes.licence) body['licence'] = attributes.licence
383 if (attributes.language) body['language'] = attributes.language
384 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
385 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
386 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
387 if (attributes.description) body['description'] = attributes.description
388 if (attributes.tags) body['tags'] = attributes.tags
389 if (attributes.privacy) body['privacy'] = attributes.privacy
390 if (attributes.channelId) body['channelId'] = attributes.channelId
391 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
392
393 // Upload request
394 if (attributes.thumbnailfile || attributes.previewfile) {
395 const attaches: any = {}
396 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
397 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
398
399 return makeUploadRequest({
400 url,
401 method: 'PUT',
402 path,
403 token: accessToken,
404 fields: body,
405 attaches,
406 statusCodeExpected
407 })
408 }
409
410 return makePutBodyRequest({
411 url,
412 path,
413 fields: body,
414 token: accessToken,
415 statusCodeExpected
416 })
417}
418
419function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
420 const path = '/api/v1/videos/' + id + '/rate'
421
422 return request(url)
423 .put(path)
424 .set('Accept', 'application/json')
425 .set('Authorization', 'Bearer ' + accessToken)
426 .send({ rating })
427 .expect(specialStatus)
428}
429
430function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
431 return new Promise<any>((res, rej) => {
432 const torrentName = videoUUID + '-' + resolution + '.torrent'
433 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
434 readFile(torrentPath, (err, data) => {
435 if (err) return rej(err)
436
437 return res(parseTorrent(data))
438 })
439 })
440}
441
442async function completeVideoCheck (
443 url: string,
444 video: any,
445 attributes: {
446 name: string
447 category: number
448 licence: number
449 language: string
450 nsfw: boolean
451 commentsEnabled: boolean
452 downloadEnabled: boolean
453 description: string
454 publishedAt?: string
455 support: string
456 account: {
457 name: string
458 host: string
459 }
460 isLocal: boolean
461 tags: string[]
462 privacy: number
463 likes?: number
464 dislikes?: number
465 duration: number
466 channel: {
467 displayName: string
468 name: string
469 description
470 isLocal: boolean
471 }
472 fixture: string
473 files: {
474 resolution: number
475 size: number
476 }[],
477 thumbnailfile?: string
478 previewfile?: string
479 }
480) {
481 if (!attributes.likes) attributes.likes = 0
482 if (!attributes.dislikes) attributes.dislikes = 0
483
484 expect(video.name).to.equal(attributes.name)
485 expect(video.category.id).to.equal(attributes.category)
486 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
487 expect(video.licence.id).to.equal(attributes.licence)
488 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
489 expect(video.language.id).to.equal(attributes.language)
490 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
491 expect(video.privacy.id).to.deep.equal(attributes.privacy)
492 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
493 expect(video.nsfw).to.equal(attributes.nsfw)
494 expect(video.description).to.equal(attributes.description)
495 expect(video.account.id).to.be.a('number')
496 expect(video.account.uuid).to.be.a('string')
497 expect(video.account.host).to.equal(attributes.account.host)
498 expect(video.account.name).to.equal(attributes.account.name)
499 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
500 expect(video.channel.name).to.equal(attributes.channel.name)
501 expect(video.likes).to.equal(attributes.likes)
502 expect(video.dislikes).to.equal(attributes.dislikes)
503 expect(video.isLocal).to.equal(attributes.isLocal)
504 expect(video.duration).to.equal(attributes.duration)
505 expect(dateIsValid(video.createdAt)).to.be.true
506 expect(dateIsValid(video.publishedAt)).to.be.true
507 expect(dateIsValid(video.updatedAt)).to.be.true
508
509 if (attributes.publishedAt) {
510 expect(video.publishedAt).to.equal(attributes.publishedAt)
511 }
512
513 const res = await getVideo(url, video.uuid)
514 const videoDetails: VideoDetails = res.body
515
516 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
517 expect(videoDetails.tags).to.deep.equal(attributes.tags)
518 expect(videoDetails.account.name).to.equal(attributes.account.name)
519 expect(videoDetails.account.host).to.equal(attributes.account.host)
520 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
521 expect(video.channel.name).to.equal(attributes.channel.name)
522 expect(videoDetails.channel.host).to.equal(attributes.account.host)
523 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
524 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
525 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
526 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
527 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
528
529 for (const attributeFile of attributes.files) {
530 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
531 expect(file).not.to.be.undefined
532
533 let extension = extname(attributes.fixture)
534 // Transcoding enabled on server 2, extension will always be .mp4
535 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
536
537 const magnetUri = file.magnetUri
538 expect(file.magnetUri).to.have.lengthOf.above(2)
539 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
540 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
541 expect(file.resolution.id).to.equal(attributeFile.resolution)
542 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
543
544 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
545 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
546 expect(file.size,
547 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
548 .to.be.above(minSize).and.below(maxSize)
549
550 {
551 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
552 }
553
554 if (attributes.previewfile) {
555 await testImage(url, attributes.previewfile, videoDetails.previewPath)
556 }
557
558 const torrent = await webtorrentAdd(magnetUri, true)
559 expect(torrent.files).to.be.an('array')
560 expect(torrent.files.length).to.equal(1)
561 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
562 }
563}
564
565// ---------------------------------------------------------------------------
566
567export {
568 getVideoDescription,
569 getVideoCategories,
570 getVideoLicences,
571 getVideoPrivacies,
572 getVideoLanguages,
573 getMyVideos,
574 getAccountVideos,
575 getVideoChannelVideos,
576 getVideo,
577 getVideoWithToken,
578 getVideosList,
579 getVideosListPagination,
580 getVideosListSort,
581 removeVideo,
582 getVideosListWithToken,
583 uploadVideo,
584 getVideosWithFilters,
585 updateVideo,
586 rateVideo,
587 viewVideo,
588 parseTorrentVideo,
589 getLocalVideos,
590 completeVideoCheck,
591 checkVideoFilesWereRemoved
592}