aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-blocklist.ts111
-rw-r--r--server/models/account/account.ts3
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/account/user.ts33
-rw-r--r--server/models/activitypub/actor-follow.ts90
-rw-r--r--server/models/redundancy/video-redundancy.ts1
-rw-r--r--server/models/server/server-blocklist.ts121
-rw-r--r--server/models/server/server.ts6
-rw-r--r--server/models/utils.ts16
-rw-r--r--server/models/video/video-comment.ts95
-rw-r--r--server/models/video/video-format-utils.ts13
-rw-r--r--server/models/video/video.ts137
12 files changed, 598 insertions, 83 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
new file mode 100644
index 000000000..fa2819235
--- /dev/null
+++ b/server/models/account/account-blocklist.ts
@@ -0,0 +1,111 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5
6enum ScopeNames {
7 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
8}
9
10@Scopes({
11 [ScopeNames.WITH_ACCOUNTS]: {
12 include: [
13 {
14 model: () => AccountModel,
15 required: true,
16 as: 'ByAccount'
17 },
18 {
19 model: () => AccountModel,
20 required: true,
21 as: 'BlockedAccount'
22 }
23 ]
24 }
25})
26
27@Table({
28 tableName: 'accountBlocklist',
29 indexes: [
30 {
31 fields: [ 'accountId', 'targetAccountId' ],
32 unique: true
33 },
34 {
35 fields: [ 'targetAccountId' ]
36 }
37 ]
38})
39export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
40
41 @CreatedAt
42 createdAt: Date
43
44 @UpdatedAt
45 updatedAt: Date
46
47 @ForeignKey(() => AccountModel)
48 @Column
49 accountId: number
50
51 @BelongsTo(() => AccountModel, {
52 foreignKey: {
53 name: 'accountId',
54 allowNull: false
55 },
56 as: 'ByAccount',
57 onDelete: 'CASCADE'
58 })
59 ByAccount: AccountModel
60
61 @ForeignKey(() => AccountModel)
62 @Column
63 targetAccountId: number
64
65 @BelongsTo(() => AccountModel, {
66 foreignKey: {
67 name: 'targetAccountId',
68 allowNull: false
69 },
70 as: 'BlockedAccount',
71 onDelete: 'CASCADE'
72 })
73 BlockedAccount: AccountModel
74
75 static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
76 const query = {
77 where: {
78 accountId,
79 targetAccountId
80 }
81 }
82
83 return AccountBlocklistModel.findOne(query)
84 }
85
86 static listForApi (accountId: number, start: number, count: number, sort: string) {
87 const query = {
88 offset: start,
89 limit: count,
90 order: getSort(sort),
91 where: {
92 accountId
93 }
94 }
95
96 return AccountBlocklistModel
97 .scope([ ScopeNames.WITH_ACCOUNTS ])
98 .findAndCountAll(query)
99 .then(({ rows, count }) => {
100 return { total: count, data: rows }
101 })
102 }
103
104 toFormattedJSON (): AccountBlock {
105 return {
106 byAccount: this.ByAccount.toFormattedJSON(),
107 blockedAccount: this.BlockedAccount.toFormattedJSON(),
108 createdAt: this.createdAt
109 }
110 }
111}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 27c75d886..5a237d733 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> {
248 displayName: this.getDisplayName(), 248 displayName: this.getDisplayName(),
249 description: this.description, 249 description: this.description,
250 createdAt: this.createdAt, 250 createdAt: this.createdAt,
251 updatedAt: this.updatedAt 251 updatedAt: this.updatedAt,
252 userId: this.userId ? this.userId : undefined
252 } 253 }
253 254
254 return Object.assign(actor, account) 255 return Object.assign(actor, account)
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
new file mode 100644
index 000000000..0476cad9d
--- /dev/null
+++ b/server/models/account/user-video-history.ts
@@ -0,0 +1,55 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4
5@Table({
6 tableName: 'userVideoHistory',
7 indexes: [
8 {
9 fields: [ 'userId', 'videoId' ],
10 unique: true
11 },
12 {
13 fields: [ 'userId' ]
14 },
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @IsInt
29 @Column
30 currentTime: number
31
32 @ForeignKey(() => VideoModel)
33 @Column
34 videoId: number
35
36 @BelongsTo(() => VideoModel, {
37 foreignKey: {
38 allowNull: false
39 },
40 onDelete: 'CASCADE'
41 })
42 Video: VideoModel
43
44 @ForeignKey(() => UserModel)
45 @Column
46 userId: number
47
48 @BelongsTo(() => UserModel, {
49 foreignKey: {
50 allowNull: false
51 },
52 onDelete: 'CASCADE'
53 })
54 User: UserModel
55}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e56b0bf40..34aafa1a7 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -31,7 +31,8 @@ import {
31 isUserRoleValid, 31 isUserRoleValid,
32 isUserUsernameValid, 32 isUserUsernameValid,
33 isUserVideoQuotaDailyValid, 33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid 34 isUserVideoQuotaValid,
35 isUserWebTorrentEnabledValid
35} from '../../helpers/custom-validators/users' 36} from '../../helpers/custom-validators/users'
36import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 37import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
37import { OAuthTokenModel } from '../oauth/oauth-token' 38import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -109,6 +110,12 @@ export class UserModel extends Model<UserModel> {
109 110
110 @AllowNull(false) 111 @AllowNull(false)
111 @Default(true) 112 @Default(true)
113 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
114 @Column
115 webTorrentEnabled: boolean
116
117 @AllowNull(false)
118 @Default(true)
112 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) 119 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
113 @Column 120 @Column
114 autoPlayVideo: boolean 121 autoPlayVideo: boolean
@@ -181,7 +188,25 @@ export class UserModel extends Model<UserModel> {
181 return this.count() 188 return this.count()
182 } 189 }
183 190
184 static listForApi (start: number, count: number, sort: string) { 191 static listForApi (start: number, count: number, sort: string, search?: string) {
192 let where = undefined
193 if (search) {
194 where = {
195 [Sequelize.Op.or]: [
196 {
197 email: {
198 [Sequelize.Op.iLike]: '%' + search + '%'
199 }
200 },
201 {
202 username: {
203 [ Sequelize.Op.iLike ]: '%' + search + '%'
204 }
205 }
206 ]
207 }
208 }
209
185 const query = { 210 const query = {
186 attributes: { 211 attributes: {
187 include: [ 212 include: [
@@ -204,7 +229,8 @@ export class UserModel extends Model<UserModel> {
204 }, 229 },
205 offset: start, 230 offset: start,
206 limit: count, 231 limit: count,
207 order: getSort(sort) 232 order: getSort(sort),
233 where
208 } 234 }
209 235
210 return UserModel.findAndCountAll(query) 236 return UserModel.findAndCountAll(query)
@@ -336,6 +362,7 @@ export class UserModel extends Model<UserModel> {
336 email: this.email, 362 email: this.email,
337 emailVerified: this.emailVerified, 363 emailVerified: this.emailVerified,
338 nsfwPolicy: this.nsfwPolicy, 364 nsfwPolicy: this.nsfwPolicy,
365 webTorrentEnabled: this.webTorrentEnabled,
339 autoPlayVideo: this.autoPlayVideo, 366 autoPlayVideo: this.autoPlayVideo,
340 role: this.role, 367 role: this.role,
341 roleLabel: USER_ROLE_LABELS[ this.role ], 368 roleLabel: USER_ROLE_LABELS[ this.role ],
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 27bb43dae..3373355ef 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -280,7 +280,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
280 return ActorFollowModel.findAll(query) 280 return ActorFollowModel.findAll(query)
281 } 281 }
282 282
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 283 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
284 const query = { 284 const query = {
285 distinct: true, 285 distinct: true,
286 offset: start, 286 offset: start,
@@ -299,7 +299,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
299 model: ActorModel, 299 model: ActorModel,
300 as: 'ActorFollowing', 300 as: 'ActorFollowing',
301 required: true, 301 required: true,
302 include: [ ServerModel ] 302 include: [
303 {
304 model: ServerModel,
305 required: true,
306 where: search ? {
307 host: {
308 [Sequelize.Op.iLike]: '%' + search + '%'
309 }
310 } : undefined
311 }
312 ]
303 } 313 }
304 ] 314 ]
305 } 315 }
@@ -313,6 +323,49 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
313 }) 323 })
314 } 324 }
315 325
326 static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) {
327 const query = {
328 distinct: true,
329 offset: start,
330 limit: count,
331 order: getSort(sort),
332 include: [
333 {
334 model: ActorModel,
335 required: true,
336 as: 'ActorFollower',
337 include: [
338 {
339 model: ServerModel,
340 required: true,
341 where: search ? {
342 host: {
343 [ Sequelize.Op.iLike ]: '%' + search + '%'
344 }
345 } : undefined
346 }
347 ]
348 },
349 {
350 model: ActorModel,
351 as: 'ActorFollowing',
352 required: true,
353 where: {
354 id
355 }
356 }
357 ]
358 }
359
360 return ActorFollowModel.findAndCountAll(query)
361 .then(({ rows, count }) => {
362 return {
363 data: rows,
364 total: count
365 }
366 })
367 }
368
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 369 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
317 const query = { 370 const query = {
318 attributes: [], 371 attributes: [],
@@ -370,39 +423,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
370 }) 423 })
371 } 424 }
372 425
373 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
374 const query = {
375 distinct: true,
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 include: [
380 {
381 model: ActorModel,
382 required: true,
383 as: 'ActorFollower',
384 include: [ ServerModel ]
385 },
386 {
387 model: ActorModel,
388 as: 'ActorFollowing',
389 required: true,
390 where: {
391 id
392 }
393 }
394 ]
395 }
396
397 return ActorFollowModel.findAndCountAll(query)
398 .then(({ rows, count }) => {
399 return {
400 data: rows,
401 total: count
402 }
403 })
404 }
405
406 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 426 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
407 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 427 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
408 } 428 }
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 2ebe23ef1..cbfc7f7fa 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -408,6 +408,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
408 url: { 408 url: {
409 type: 'Link', 409 type: 'Link',
410 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 410 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
411 mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
411 href: this.fileUrl, 412 href: this.fileUrl,
412 height: this.VideoFile.resolution, 413 height: this.VideoFile.resolution,
413 size: this.VideoFile.size, 414 size: this.VideoFile.size,
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
new file mode 100644
index 000000000..450f27152
--- /dev/null
+++ b/server/models/server/server-blocklist.ts
@@ -0,0 +1,121 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort } from '../utils'
6
7enum ScopeNames {
8 WITH_ACCOUNT = 'WITH_ACCOUNT',
9 WITH_SERVER = 'WITH_SERVER'
10}
11
12@Scopes({
13 [ScopeNames.WITH_ACCOUNT]: {
14 include: [
15 {
16 model: () => AccountModel,
17 required: true
18 }
19 ]
20 },
21 [ScopeNames.WITH_SERVER]: {
22 include: [
23 {
24 model: () => ServerModel,
25 required: true
26 }
27 ]
28 }
29})
30
31@Table({
32 tableName: 'serverBlocklist',
33 indexes: [
34 {
35 fields: [ 'accountId', 'targetServerId' ],
36 unique: true
37 },
38 {
39 fields: [ 'targetServerId' ]
40 }
41 ]
42})
43export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
44
45 @CreatedAt
46 createdAt: Date
47
48 @UpdatedAt
49 updatedAt: Date
50
51 @ForeignKey(() => AccountModel)
52 @Column
53 accountId: number
54
55 @BelongsTo(() => AccountModel, {
56 foreignKey: {
57 name: 'accountId',
58 allowNull: false
59 },
60 onDelete: 'CASCADE'
61 })
62 ByAccount: AccountModel
63
64 @ForeignKey(() => ServerModel)
65 @Column
66 targetServerId: number
67
68 @BelongsTo(() => ServerModel, {
69 foreignKey: {
70 name: 'targetServerId',
71 allowNull: false
72 },
73 onDelete: 'CASCADE'
74 })
75 BlockedServer: ServerModel
76
77 static loadByAccountAndHost (accountId: number, host: string) {
78 const query = {
79 where: {
80 accountId
81 },
82 include: [
83 {
84 model: ServerModel,
85 where: {
86 host
87 },
88 required: true
89 }
90 ]
91 }
92
93 return ServerBlocklistModel.findOne(query)
94 }
95
96 static listForApi (accountId: number, start: number, count: number, sort: string) {
97 const query = {
98 offset: start,
99 limit: count,
100 order: getSort(sort),
101 where: {
102 accountId
103 }
104 }
105
106 return ServerBlocklistModel
107 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
108 .findAndCountAll(query)
109 .then(({ rows, count }) => {
110 return { total: count, data: rows }
111 })
112 }
113
114 toFormattedJSON (): ServerBlock {
115 return {
116 byAccount: this.ByAccount.toFormattedJSON(),
117 blockedServer: this.BlockedServer.toFormattedJSON(),
118 createdAt: this.createdAt
119 }
120 }
121}
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index ca3b24d51..300d70938 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> {
49 49
50 return ServerModel.findOne(query) 50 return ServerModel.findOne(query)
51 } 51 }
52
53 toFormattedJSON () {
54 return {
55 host: this.host
56 }
57 }
52} 58}
diff --git a/server/models/utils.ts b/server/models/utils.ts
index e0bf091ad..60b0906e8 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -64,9 +64,25 @@ function createSimilarityAttribute (col: string, value: string) {
64 ) 64 )
65} 65}
66 66
67function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) {
68 const blockerIds = [ serverAccountId ]
69 if (userAccountId) blockerIds.push(userAccountId)
70
71 const blockerIdsString = blockerIds.join(', ')
72
73 const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
74 ' UNION ALL ' +
75 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
76 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
77 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
78
79 return query
80}
81
67// --------------------------------------------------------------------------- 82// ---------------------------------------------------------------------------
68 83
69export { 84export {
85 buildBlockedAccountSQL,
70 SortType, 86 SortType,
71 getSort, 87 getSort,
72 getVideoSort, 88 getVideoSort,
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index f84c1880c..dd6d08139 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,6 +1,17 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { 2import {
3 AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, 3 AllowNull,
4 BeforeDestroy,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 ForeignKey,
10 IFindOptions,
11 Is,
12 Model,
13 Scopes,
14 Table,
4 UpdatedAt 15 UpdatedAt
5} from 'sequelize-typescript' 16} from 'sequelize-typescript'
6import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' 17import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
@@ -13,9 +24,11 @@ import { AccountModel } from '../account/account'
13import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
14import { AvatarModel } from '../avatar/avatar' 25import { AvatarModel } from '../avatar/avatar'
15import { ServerModel } from '../server/server' 26import { ServerModel } from '../server/server'
16import { getSort, throwIfNotValid } from '../utils' 27import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
17import { VideoModel } from './video' 28import { VideoModel } from './video'
18import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user'
19 32
20enum ScopeNames { 33enum ScopeNames {
21 WITH_ACCOUNT = 'WITH_ACCOUNT', 34 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -25,18 +38,29 @@ enum ScopeNames {
25} 38}
26 39
27@Scopes({ 40@Scopes({
28 [ScopeNames.ATTRIBUTES_FOR_API]: { 41 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
29 attributes: { 42 return {
30 include: [ 43 attributes: {
31 [ 44 include: [
32 Sequelize.literal( 45 [
33 '(SELECT COUNT("replies"."id") ' + 46 Sequelize.literal(
34 'FROM "videoComment" AS "replies" ' + 47 '(' +
35 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' 48 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
36 ), 49 'SELECT COUNT("replies"."id") - (' +
37 'totalReplies' 50 'SELECT COUNT("replies"."id") ' +
51 'FROM "videoComment" AS "replies" ' +
52 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
53 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
54 ')' +
55 'FROM "videoComment" AS "replies" ' +
56 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
57 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
58 ')'
59 ),
60 'totalReplies'
61 ]
38 ] 62 ]
39 ] 63 }
40 } 64 }
41 }, 65 },
42 [ScopeNames.WITH_ACCOUNT]: { 66 [ScopeNames.WITH_ACCOUNT]: {
@@ -267,26 +291,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
267 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) 291 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
268 } 292 }
269 293
270 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { 294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
295 const serverActor = await getServerActor()
296 const serverAccountId = serverActor.Account.id
297 const userAccountId = user ? user.Account.id : undefined
298
271 const query = { 299 const query = {
272 offset: start, 300 offset: start,
273 limit: count, 301 limit: count,
274 order: getSort(sort), 302 order: getSort(sort),
275 where: { 303 where: {
276 videoId, 304 videoId,
277 inReplyToCommentId: null 305 inReplyToCommentId: null,
306 accountId: {
307 [Sequelize.Op.notIn]: Sequelize.literal(
308 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
309 )
310 }
278 } 311 }
279 } 312 }
280 313
314 // FIXME: typings
315 const scopes: any[] = [
316 ScopeNames.WITH_ACCOUNT,
317 {
318 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
319 }
320 ]
321
281 return VideoCommentModel 322 return VideoCommentModel
282 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 323 .scope(scopes)
283 .findAndCountAll(query) 324 .findAndCountAll(query)
284 .then(({ rows, count }) => { 325 .then(({ rows, count }) => {
285 return { total: count, data: rows } 326 return { total: count, data: rows }
286 }) 327 })
287 } 328 }
288 329
289 static listThreadCommentsForApi (videoId: number, threadId: number) { 330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
331 const serverActor = await getServerActor()
332 const serverAccountId = serverActor.Account.id
333 const userAccountId = user ? user.Account.id : undefined
334
290 const query = { 335 const query = {
291 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
292 where: { 337 where: {
@@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
294 [ Sequelize.Op.or ]: [ 339 [ Sequelize.Op.or ]: [
295 { id: threadId }, 340 { id: threadId },
296 { originCommentId: threadId } 341 { originCommentId: threadId }
297 ] 342 ],
343 accountId: {
344 [Sequelize.Op.notIn]: Sequelize.literal(
345 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
346 )
347 }
298 } 348 }
299 } 349 }
300 350
351 const scopes: any[] = [
352 ScopeNames.WITH_ACCOUNT,
353 {
354 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
355 }
356 ]
357
301 return VideoCommentModel 358 return VideoCommentModel
302 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 359 .scope(scopes)
303 .findAndCountAll(query) 360 .findAndCountAll(query)
304 .then(({ rows, count }) => { 361 .then(({ rows, count }) => {
305 return { total: count, data: rows } 362 return { total: count, data: rows }
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..e3f8d525b 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,6 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc'
13 14
14export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
15 completeDescription?: boolean 16 completeDescription?: boolean
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
24 const formattedAccount = video.VideoChannel.Account.toFormattedJSON() 25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
25 const formattedVideoChannel = video.VideoChannel.toFormattedJSON() 26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
26 27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29
27 const videoObject: Video = { 30 const videoObject: Video = {
28 id: video.id, 31 id: video.id,
29 uuid: video.uuid, 32 uuid: video.uuid,
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
74 url: formattedVideoChannel.url, 77 url: formattedVideoChannel.url,
75 host: formattedVideoChannel.host, 78 host: formattedVideoChannel.host,
76 avatar: formattedVideoChannel.avatar 79 avatar: formattedVideoChannel.avatar
77 } 80 },
81
82 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime
84 } : undefined
78 } 85 }
79 86
80 if (options) { 87 if (options) {
@@ -201,6 +208,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
201 url.push({ 208 url.push({
202 type: 'Link', 209 type: 'Link',
203 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 210 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
211 mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
204 href: video.getVideoFileUrl(file, baseUrlHttp), 212 href: video.getVideoFileUrl(file, baseUrlHttp),
205 height: file.resolution, 213 height: file.resolution,
206 size: file.size, 214 size: file.size,
@@ -210,6 +218,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
210 url.push({ 218 url.push({
211 type: 'Link', 219 type: 'Link',
212 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', 220 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
221 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
213 href: video.getTorrentUrl(file, baseUrlHttp), 222 href: video.getTorrentUrl(file, baseUrlHttp),
214 height: file.resolution 223 height: file.resolution
215 }) 224 })
@@ -217,6 +226,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
217 url.push({ 226 url.push({
218 type: 'Link', 227 type: 'Link',
219 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', 228 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
229 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
220 href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), 230 href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
221 height: file.resolution 231 height: file.resolution
222 }) 232 })
@@ -226,6 +236,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
226 url.push({ 236 url.push({
227 type: 'Link', 237 type: 'Link',
228 mimeType: 'text/html', 238 mimeType: 'text/html',
239 mediaType: 'text/html',
229 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 240 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
230 }) 241 })
231 242
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c89c16bf..6c183933b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -27,7 +27,7 @@ import {
27 Table, 27 Table,
28 UpdatedAt 28 UpdatedAt
29} from 'sequelize-typescript' 29} from 'sequelize-typescript'
30import { VideoPrivacy, VideoState } from '../../../shared' 30import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
33import { VideoFilter } from '../../../shared/models/videos/video-query.type' 33import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
70import { ActorModel } from '../activitypub/actor' 70import { ActorModel } from '../activitypub/actor'
71import { AvatarModel } from '../avatar/avatar' 71import { AvatarModel } from '../avatar/avatar'
72import { ServerModel } from '../server/server' 72import { ServerModel } from '../server/server'
73import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' 73import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
74import { TagModel } from './tag' 74import { TagModel } from './tag'
75import { VideoAbuseModel } from './video-abuse' 75import { VideoAbuseModel } from './video-abuse'
76import { VideoChannelModel } from './video-channel' 76import { VideoChannelModel } from './video-channel'
@@ -92,6 +92,8 @@ import {
92 videoModelToFormattedJSON 92 videoModelToFormattedJSON
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user'
95 97
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +129,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 129 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 130 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 131 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 132 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
133 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 134}
132 135
133type ForAPIOptions = { 136type ForAPIOptions = {
@@ -136,6 +139,7 @@ type ForAPIOptions = {
136} 139}
137 140
138type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number
139 actorId: number 143 actorId: number
140 includeLocalVideos: boolean 144 includeLocalVideos: boolean
141 filter?: VideoFilter 145 filter?: VideoFilter
@@ -149,6 +153,7 @@ type AvailableForListIDsOptions = {
149 accountId?: number 153 accountId?: number
150 videoChannelId?: number 154 videoChannelId?: number
151 trendingDays?: number 155 trendingDays?: number
156 user?: UserModel
152} 157}
153 158
154@Scopes({ 159@Scopes({
@@ -234,6 +239,22 @@ type AvailableForListIDsOptions = {
234 } 239 }
235 ] 240 ]
236 }, 241 },
242 channelId: {
243 [ Sequelize.Op.notIn ]: Sequelize.literal(
244 '(' +
245 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
246 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
247 ')' +
248 ')'
249 )
250 }
251 },
252 include: []
253 }
254
255 // Only list public/published videos
256 if (!options.filter || options.filter !== 'all-local') {
257 const privacyWhere = {
237 // Always list public videos 258 // Always list public videos
238 privacy: VideoPrivacy.PUBLIC, 259 privacy: VideoPrivacy.PUBLIC,
239 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 260 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -248,8 +269,9 @@ type AvailableForListIDsOptions = {
248 } 269 }
249 } 270 }
250 ] 271 ]
251 }, 272 }
252 include: [] 273
274 Object.assign(query.where, privacyWhere)
253 } 275 }
254 276
255 if (options.filter || options.accountId || options.videoChannelId) { 277 if (options.filter || options.accountId || options.videoChannelId) {
@@ -464,6 +486,8 @@ type AvailableForListIDsOptions = {
464 include: [ 486 include: [
465 { 487 {
466 model: () => VideoFileModel.unscoped(), 488 model: () => VideoFileModel.unscoped(),
489 // FIXME: typings
490 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 491 required: false,
468 include: [ 492 include: [
469 { 493 {
@@ -482,6 +506,20 @@ type AvailableForListIDsOptions = {
482 required: false 506 required: false
483 } 507 }
484 ] 508 ]
509 },
510 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
511 return {
512 include: [
513 {
514 attributes: [ 'currentTime' ],
515 model: UserVideoHistoryModel.unscoped(),
516 required: false,
517 where: {
518 userId
519 }
520 }
521 ]
522 }
485 } 523 }
486}) 524})
487@Table({ 525@Table({
@@ -672,11 +710,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 710 name: 'videoId',
673 allowNull: false 711 allowNull: false
674 }, 712 },
675 onDelete: 'cascade', 713 onDelete: 'cascade'
676 hooks: true
677 }) 714 })
678 VideoViews: VideoViewModel[] 715 VideoViews: VideoViewModel[]
679 716
717 @HasMany(() => UserVideoHistoryModel, {
718 foreignKey: {
719 name: 'videoId',
720 allowNull: false
721 },
722 onDelete: 'cascade'
723 })
724 UserVideoHistories: UserVideoHistoryModel[]
725
680 @HasOne(() => ScheduleVideoUpdateModel, { 726 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 727 foreignKey: {
682 name: 'videoId', 728 name: 'videoId',
@@ -762,6 +808,16 @@ export class VideoModel extends Model<VideoModel> {
762 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 808 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
763 } 809 }
764 810
811 static listLocal () {
812 const query = {
813 where: {
814 remote: false
815 }
816 }
817
818 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query)
819 }
820
765 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 821 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
766 function getRawQuery (select: string) { 822 function getRawQuery (select: string) {
767 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + 823 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
@@ -930,8 +986,13 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 986 accountId?: number,
931 videoChannelId?: number, 987 videoChannelId?: number,
932 actorId?: number 988 actorId?: number
933 trendingDays?: number 989 trendingDays?: number,
990 user?: UserModel
934 }, countVideos = true) { 991 }, countVideos = true) {
992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
993 throw new Error('Try to filter all-local but no user has not the see all videos right')
994 }
995
935 const query: IFindOptions<VideoModel> = { 996 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 997 offset: options.start,
937 limit: options.count, 998 limit: options.count,
@@ -945,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
945 query.group = 'VideoModel.id' 1006 query.group = 'VideoModel.id'
946 } 1007 }
947 1008
1009 const serverActor = await getServerActor()
1010
948 // actorId === null has a meaning, so just check undefined 1011 // actorId === null has a meaning, so just check undefined
949 const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id 1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
950 1013
951 const queryOptions = { 1014 const queryOptions = {
952 actorId, 1015 actorId,
1016 serverAccountId: serverActor.Account.id,
953 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
954 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
955 licenceOneOf: options.licenceOneOf, 1019 licenceOneOf: options.licenceOneOf,
@@ -961,6 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 1025 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 1026 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 1027 includeLocalVideos: options.includeLocalVideos,
1028 user: options.user,
964 trendingDays 1029 trendingDays
965 } 1030 }
966 1031
@@ -983,6 +1048,8 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1048 tagsAllOf?: string[]
984 durationMin?: number // seconds 1049 durationMin?: number // seconds
985 durationMax?: number // seconds 1050 durationMax?: number // seconds
1051 user?: UserModel,
1052 filter?: VideoFilter
986 }) { 1053 }) {
987 const whereAnd = [] 1054 const whereAnd = []
988 1055
@@ -1052,13 +1119,16 @@ export class VideoModel extends Model<VideoModel> {
1052 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1053 const queryOptions = { 1120 const queryOptions = {
1054 actorId: serverActor.id, 1121 actorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id,
1055 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1056 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
1057 categoryOneOf: options.categoryOneOf, 1125 categoryOneOf: options.categoryOneOf,
1058 licenceOneOf: options.licenceOneOf, 1126 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1127 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1128 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1129 tagsAllOf: options.tagsAllOf,
1130 user: options.user,
1131 filter: options.filter
1062 } 1132 }
1063 1133
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1134 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1195,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1195 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1196 }
1127 1197
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1198 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1199 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1200
1131 const options = { 1201 const options = {
@@ -1134,14 +1204,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1204 transaction: t
1135 } 1205 }
1136 1206
1207 const scopes = [
1208 ScopeNames.WITH_TAGS,
1209 ScopeNames.WITH_BLACKLISTED,
1210 ScopeNames.WITH_FILES,
1211 ScopeNames.WITH_ACCOUNT_DETAILS,
1212 ScopeNames.WITH_SCHEDULED_UPDATE
1213 ]
1214
1215 if (userId) {
1216 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1217 }
1218
1137 return VideoModel 1219 return VideoModel
1138 .scope([ 1220 .scope(scopes)
1139 ScopeNames.WITH_TAGS,
1140 ScopeNames.WITH_BLACKLISTED,
1141 ScopeNames.WITH_FILES,
1142 ScopeNames.WITH_ACCOUNT_DETAILS,
1143 ScopeNames.WITH_SCHEDULED_UPDATE
1144 ])
1145 .findOne(options) 1221 .findOne(options)
1146 } 1222 }
1147 1223
@@ -1179,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> {
1179 1255
1180 // threshold corresponds to how many video the field should have to be returned 1256 // threshold corresponds to how many video the field should have to be returned
1181 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1182 const actorId = (await getServerActor()).id 1258 const serverActor = await getServerActor()
1259 const actorId = serverActor.id
1183 1260
1184 const scopeOptions = { 1261 const scopeOptions: AvailableForListIDsOptions = {
1262 serverAccountId: serverActor.Account.id,
1185 actorId, 1263 actorId,
1186 includeLocalVideos: true 1264 includeLocalVideos: true
1187 } 1265 }
@@ -1216,7 +1294,7 @@ export class VideoModel extends Model<VideoModel> {
1216 } 1294 }
1217 1295
1218 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1296 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1219 if (filter && filter === 'local') { 1297 if (filter && (filter === 'local' || filter === 'all-local')) {
1220 return { 1298 return {
1221 serverId: null 1299 serverId: null
1222 } 1300 }
@@ -1225,7 +1303,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1303 return {}
1226 } 1304 }
1227 1305
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1306 private static async getAvailableForApi (
1307 query: IFindOptions<VideoModel>,
1308 options: AvailableForListIDsOptions,
1309 countVideos = true
1310 ) {
1229 const idsScope = { 1311 const idsScope = {
1230 method: [ 1312 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1313 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1331,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1331
1250 if (ids.length === 0) return { data: [], total: count } 1332 if (ids.length === 0) return { data: [], total: count }
1251 1333
1252 const apiScope = { 1334 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1335 const apiScope: any[] = [
1336 {
1337 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1338 }
1339 ]
1340
1341 if (options.user) {
1342 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1254 } 1343 }
1255 1344
1256 const secondQuery = { 1345 const secondQuery = {