From b9fffa297f49a84df8ffd0d7b842599bc88a8e3e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 4 Dec 2018 17:08:55 +0100 Subject: Create redundancy endpoint --- server/models/redundancy/video-redundancy.ts | 4 ++-- server/models/video/video.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'server/models') diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 9de4356b4..dd37dad22 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -15,7 +15,7 @@ import { import { ActorModel } from '../activitypub/actor' import { getVideoSort, throwIfNotValid } from '../utils' import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' +import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers' import { VideoFileModel } from '../video/video-file' import { getServerActor } from '../../helpers/utils' import { VideoModel } from '../video/video' @@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model { const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` logger.info('Removing duplicated video file %s.', logIdentifier) - videoFile.Video.removeFile(videoFile) + videoFile.Video.removeFile(videoFile, true) .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) return undefined diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0f18d9f0c..e8cb5aa88 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1538,8 +1538,10 @@ export class VideoModel extends Model { .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) } - removeFile (videoFile: VideoFileModel) { - const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) + removeFile (videoFile: VideoFileModel, isRedundancy = false) { + const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR + + const filePath = join(baseDir, this.getVideoFilename(videoFile)) return remove(filePath) .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) } @@ -1617,6 +1619,10 @@ export class VideoModel extends Model { return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) } + getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { + return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) + } + getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) } -- cgit v1.2.3 From 4e74e8032be8293ffe3cb3c30528d4ef7c11a798 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Dec 2018 14:36:05 +0100 Subject: Remove inferred type --- server/models/video/video.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'server/models') diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e8cb5aa88..adef37937 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -140,7 +140,7 @@ type ForAPIOptions = { type AvailableForListIDsOptions = { serverAccountId: number - actorId: number + followerActorId: number includeLocalVideos: boolean filter?: VideoFilter categoryOneOf?: number[] @@ -315,7 +315,7 @@ type AvailableForListIDsOptions = { query.include.push(videoChannelInclude) } - if (options.actorId) { + if (options.followerActorId) { let localVideosReq = '' if (options.includeLocalVideos === true) { localVideosReq = ' UNION ALL ' + @@ -327,7 +327,7 @@ type AvailableForListIDsOptions = { } // Force actorId to be a number to avoid SQL injections - const actorIdNumber = parseInt(options.actorId.toString(), 10) + const actorIdNumber = parseInt(options.followerActorId.toString(), 10) query.where[ 'id' ][ Sequelize.Op.and ].push({ [ Sequelize.Op.in ]: Sequelize.literal( '(' + @@ -985,7 +985,7 @@ export class VideoModel extends Model { filter?: VideoFilter, accountId?: number, videoChannelId?: number, - actorId?: number + followerActorId?: number trendingDays?: number, user?: UserModel }, countVideos = true) { @@ -1008,11 +1008,11 @@ export class VideoModel extends Model { const serverActor = await getServerActor() - // actorId === null has a meaning, so just check undefined - const actorId = options.actorId !== undefined ? options.actorId : serverActor.id + // followerActorId === null has a meaning, so just check undefined + const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id const queryOptions = { - actorId, + followerActorId, serverAccountId: serverActor.Account.id, nsfw: options.nsfw, categoryOneOf: options.categoryOneOf, @@ -1118,7 +1118,7 @@ export class VideoModel extends Model { const serverActor = await getServerActor() const queryOptions = { - actorId: serverActor.id, + followerActorId: serverActor.id, serverAccountId: serverActor.Account.id, includeLocalVideos: options.includeLocalVideos, nsfw: options.nsfw, @@ -1273,11 +1273,11 @@ export class VideoModel extends Model { // threshold corresponds to how many video the field should have to be returned static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { const serverActor = await getServerActor() - const actorId = serverActor.id + const followerActorId = serverActor.id const scopeOptions: AvailableForListIDsOptions = { serverAccountId: serverActor.Account.id, - actorId, + followerActorId, includeLocalVideos: true } -- cgit v1.2.3 From 2feebf3e6afaad9ab80976d1557d3a7bcf94de03 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Dec 2018 17:27:24 +0100 Subject: Add sitemap --- server/models/account/account.ts | 21 +++++++++++++++++++++ server/models/video/video-channel.ts | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) (limited to 'server/models') diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 5a237d733..a99e9b1ad 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -241,6 +241,27 @@ export class AccountModel extends Model { }) } + static listLocalsForSitemap (sort: string) { + const query = { + attributes: [ ], + offset: 0, + order: getSort(sort), + include: [ + { + attributes: [ 'preferredUsername', 'serverId' ], + model: ActorModel.unscoped(), + where: { + serverId: null + } + } + ] + } + + return AccountModel + .unscoped() + .findAll(query) + } + toFormattedJSON (): Account { const actor = this.Actor.toFormattedJSON() const account = { diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f4586917e..86bf0461a 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -233,6 +233,27 @@ export class VideoChannelModel extends Model { }) } + static listLocalsForSitemap (sort: string) { + const query = { + attributes: [ ], + offset: 0, + order: getSort(sort), + include: [ + { + attributes: [ 'preferredUsername', 'serverId' ], + model: ActorModel.unscoped(), + where: { + serverId: null + } + } + ] + } + + return VideoChannelModel + .unscoped() + .findAll(query) + } + static searchForApi (options: { actorId: number search: string -- cgit v1.2.3 From 14e2014acc1362cfbb770c051a7254b156cd8efb Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 Dec 2018 14:52:50 +0100 Subject: Support additional video extensions --- server/models/redundancy/video-redundancy.ts | 6 +++--- server/models/video/video-file.ts | 4 +++- server/models/video/video-format-utils.ts | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) (limited to 'server/models') diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index dd37dad22..8b6cd146a 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -15,7 +15,7 @@ import { import { ActorModel } from '../activitypub/actor' import { getVideoSort, throwIfNotValid } from '../utils' import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers' +import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers' import { VideoFileModel } from '../video/video-file' import { getServerActor } from '../../helpers/utils' import { VideoModel } from '../video/video' @@ -415,8 +415,8 @@ export class VideoRedundancyModel extends Model { expires: this.expiresOn.toISOString(), url: { type: 'Link', - mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, - mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, + mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, + mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, href: this.fileUrl, height: this.VideoFile.resolution, size: this.VideoFile.size, diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index adebdf0c7..3fd2d5a99 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -14,6 +14,7 @@ import { UpdatedAt } from 'sequelize-typescript' import { + isVideoFileExtnameValid, isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid, @@ -58,7 +59,8 @@ export class VideoFileModel extends Model { size: number @AllowNull(false) - @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) + @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname')) + @Column extname: string @AllowNull(false) diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index e3f8d525b..de0747f22 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts @@ -2,7 +2,7 @@ import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' import { VideoModel } from './video' import { VideoFileModel } from './video-file' import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' -import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' +import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' import { VideoCaptionModel } from './video-caption' import { getVideoCommentsActivityPubUrl, @@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { for (const file of video.VideoFiles) { url.push({ type: 'Link', - mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, - mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, + mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, + mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, href: video.getVideoFileUrl(file, baseUrlHttp), height: file.resolution, size: file.size, -- cgit v1.2.3 From 8b9a525a180cc9f3a98c334cc052dcfc8f36dcd4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 17 Dec 2018 15:52:38 +0100 Subject: Add history on server side Add ability to disable, clear and list user videos history --- server/models/account/user-video-history.ts | 33 ++++++++++++++++++++++++++++- server/models/account/user.ts | 9 +++++++- server/models/utils.ts | 2 +- server/models/video/video.ts | 19 ++++++++++++++--- 4 files changed, 57 insertions(+), 6 deletions(-) (limited to 'server/models') diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts index 0476cad9d..15cb399c9 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/account/user-video-history.ts @@ -1,6 +1,7 @@ -import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' import { VideoModel } from '../video/video' import { UserModel } from './user' +import { Transaction, Op, DestroyOptions } from 'sequelize' @Table({ tableName: 'userVideoHistory', @@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model { onDelete: 'CASCADE' }) User: UserModel + + static listForApi (user: UserModel, start: number, count: number) { + return VideoModel.listForApi({ + start, + count, + sort: '-UserVideoHistories.updatedAt', + nsfw: null, // All + includeLocalVideos: true, + withFiles: false, + user, + historyOfUser: user + }) + } + + static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) { + const query: DestroyOptions = { + where: { + userId: user.id + }, + transaction: t + } + + if (beforeDate) { + query.where.updatedAt = { + [Op.lt]: beforeDate + } + } + + return UserVideoHistoryModel.destroy(query) + } } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 1843603f1..ea017c338 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -32,7 +32,8 @@ import { isUserUsernameValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid, - isUserWebTorrentEnabledValid + isUserWebTorrentEnabledValid, + isUserVideosHistoryEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' import { OAuthTokenModel } from '../oauth/oauth-token' @@ -114,6 +115,12 @@ export class UserModel extends Model { @Column webTorrentEnabled: boolean + @AllowNull(false) + @Default(true) + @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) + @Column + videosHistoryEnabled: boolean + @AllowNull(false) @Default(true) @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) diff --git a/server/models/utils.ts b/server/models/utils.ts index 60b0906e8..6694eda69 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts @@ -29,7 +29,7 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { ] } - return [ [ field, direction ], lastSort ] + return [ field.split('.').concat([ direction ]), lastSort ] } function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { diff --git a/server/models/video/video.ts b/server/models/video/video.ts index adef37937..199ea9ea4 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -153,7 +153,8 @@ type AvailableForListIDsOptions = { accountId?: number videoChannelId?: number trendingDays?: number - user?: UserModel + user?: UserModel, + historyOfUser?: UserModel } @Scopes({ @@ -416,6 +417,16 @@ type AvailableForListIDsOptions = { query.subQuery = false } + if (options.historyOfUser) { + query.include.push({ + model: UserVideoHistoryModel, + required: true, + where: { + userId: options.historyOfUser.id + } + }) + } + return query }, [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { @@ -987,7 +998,8 @@ export class VideoModel extends Model { videoChannelId?: number, followerActorId?: number trendingDays?: number, - user?: UserModel + user?: UserModel, + historyOfUser?: UserModel }, countVideos = true) { if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { throw new Error('Try to filter all-local but no user has not the see all videos right') @@ -1026,6 +1038,7 @@ export class VideoModel extends Model { videoChannelId: options.videoChannelId, includeLocalVideos: options.includeLocalVideos, user: options.user, + historyOfUser: options.historyOfUser, trendingDays } @@ -1341,7 +1354,7 @@ export class VideoModel extends Model { } const [ count, rowsId ] = await Promise.all([ - countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), + countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), VideoModel.scope(idsScope).findAll(query) ]) const ids = rowsId.map(r => r.id) -- cgit v1.2.3 From 80bfd33c0bf910e2cfdd3270b14ba9eddd90e2e8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 18 Dec 2018 09:31:09 +0100 Subject: Add history page on client --- server/models/video/video.ts | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'server/models') diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 199ea9ea4..3f282580c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -425,6 +425,11 @@ type AvailableForListIDsOptions = { userId: options.historyOfUser.id } }) + + // Even if the relation is n:m, we know that a user only have 0..1 video history + // So we won't have multiple rows for the same video + // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel + query.subQuery = false } return query -- cgit v1.2.3 From 276d96529529621d5f70473990095495f2743c29 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 18 Dec 2018 11:32:37 +0100 Subject: Add ability to disable and clear history --- server/models/account/user.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'server/models') diff --git a/server/models/account/user.ts b/server/models/account/user.ts index ea017c338..180ced810 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -370,6 +370,7 @@ export class UserModel extends Model { emailVerified: this.emailVerified, nsfwPolicy: this.nsfwPolicy, webTorrentEnabled: this.webTorrentEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, autoPlayVideo: this.autoPlayVideo, role: this.role, roleLabel: USER_ROLE_LABELS[ this.role ], -- cgit v1.2.3 From afa4374ab4084ca95e33141d55a6449304caa665 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 18 Dec 2018 11:52:20 +0100 Subject: Fix video sort --- server/models/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'server/models') diff --git a/server/models/utils.ts b/server/models/utils.ts index 6694eda69..5b4093aec 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts @@ -29,7 +29,11 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { ] } - return [ field.split('.').concat([ direction ]), lastSort ] + const firstSort = typeof field === 'string' ? + field.split('.').concat([ direction ]) : + [ field, direction ] + + return [ firstSort, lastSort ] } function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { -- cgit v1.2.3 From 2f5c6b2fc6e60502c2a8df4dc9029c1d87ebe30b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 20 Dec 2018 14:31:11 +0100 Subject: Optimize actor follow scores modifications --- server/models/activitypub/actor-follow.ts | 50 ++++++++++--------------------- 1 file changed, 16 insertions(+), 34 deletions(-) (limited to 'server/models') diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 0a6935083..994f791de 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -127,22 +127,6 @@ export class ActorFollowModel extends Model { if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) } - static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) { - if (goodInboxes.length === 0 && badInboxes.length === 0) return - - logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length) - - if (goodInboxes.length !== 0) { - ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t) - .catch(err => logger.error('Cannot increment scores of good actor follows.', { err })) - } - - if (badInboxes.length !== 0) { - ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t) - .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err })) - } - } - static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { const query = { where: { @@ -464,6 +448,22 @@ export class ActorFollowModel extends Model { } } + static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { + const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + + 'WHERE id IN (' + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + + `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + + ')' + + const options = { + type: Sequelize.QueryTypes.BULKUPDATE, + transaction: t + } + + return ActorFollowModel.sequelize.query(query, options) + } + private static async createListAcceptedFollowForApiQuery ( type: 'followers' | 'following', actorIds: number[], @@ -518,24 +518,6 @@ export class ActorFollowModel extends Model { } } - private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) { - const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',') - - const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + - 'WHERE id IN (' + - 'SELECT "actorFollow"."id" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + - 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' + - ')' - - const options = t ? { - type: Sequelize.QueryTypes.BULKUPDATE, - transaction: t - } : undefined - - return ActorFollowModel.sequelize.query(query, options) - } - private static listBadActorFollows () { const query = { where: { -- cgit v1.2.3 From 439b1744f5f50b8530cded9398d51aa4bb5ed4ff Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 20 Dec 2018 15:25:24 +0100 Subject: Optimize index sizes --- server/models/video/video.ts | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) (limited to 'server/models') diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 3f282580c..bcf327f32 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -102,16 +102,44 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ { fields: [ 'createdAt' ] }, { fields: [ 'publishedAt' ] }, { fields: [ 'duration' ] }, - { fields: [ 'category' ] }, - { fields: [ 'licence' ] }, - { fields: [ 'nsfw' ] }, - { fields: [ 'language' ] }, - { fields: [ 'waitTranscoding' ] }, - { fields: [ 'state' ] }, - { fields: [ 'remote' ] }, { fields: [ 'views' ] }, - { fields: [ 'likes' ] }, { fields: [ 'channelId' ] }, + { + fields: [ 'category' ], // We don't care videos with an unknown category + where: { + category: { + [Sequelize.Op.ne]: null + } + } + }, + { + fields: [ 'licence' ], // We don't care videos with an unknown licence + where: { + licence: { + [Sequelize.Op.ne]: null + } + } + }, + { + fields: [ 'language' ], // We don't care videos with an unknown language + where: { + language: { + [Sequelize.Op.ne]: null + } + } + }, + { + fields: [ 'nsfw' ], // Most of the videos are not NSFW + where: { + nsfw: true + } + }, + { + fields: [ 'remote' ], // Only index local videos + where: { + remote: false + } + }, { fields: [ 'uuid' ], unique: true -- cgit v1.2.3 From cef534ed53e4518fe0acf581bfe880788d42fc36 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 26 Dec 2018 10:36:24 +0100 Subject: Add user notification base code --- server/models/account/user-notification-setting.ts | 100 ++++++++ server/models/account/user-notification.ts | 256 +++++++++++++++++++++ server/models/account/user.ts | 101 +++++++- server/models/activitypub/actor-follow.ts | 14 +- server/models/activitypub/actor.ts | 1 + server/models/video/video-abuse.ts | 5 - server/models/video/video-blacklist.ts | 10 - server/models/video/video-comment.ts | 4 + server/models/video/video.ts | 4 + 9 files changed, 466 insertions(+), 29 deletions(-) create mode 100644 server/models/account/user-notification-setting.ts create mode 100644 server/models/account/user-notification.ts (limited to 'server/models') diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts new file mode 100644 index 000000000..bc24b1e33 --- /dev/null +++ b/server/models/account/user-notification-setting.ts @@ -0,0 +1,100 @@ +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + Default, + ForeignKey, + Is, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { throwIfNotValid } from '../utils' +import { UserModel } from './user' +import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' +import { clearCacheByUserId } from '../../lib/oauth-model' + +@Table({ + tableName: 'userNotificationSetting', + indexes: [ + { + fields: [ 'userId' ], + unique: true + } + ] +}) +export class UserNotificationSettingModel extends Model { + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewVideoFromSubscription', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') + ) + @Column + newVideoFromSubscription: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewCommentOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') + ) + @Column + newCommentOnMyVideo: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingVideoAbuseAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') + ) + @Column + videoAbuseAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingBlacklistOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') + ) + @Column + blacklistOnMyVideo: UserNotificationSettingValue + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserNotificationSettingModel) { + return clearCacheByUserId(instance.userId) + } + + toFormattedJSON (): UserNotificationSetting { + return { + newCommentOnMyVideo: this.newCommentOnMyVideo, + newVideoFromSubscription: this.newVideoFromSubscription, + videoAbuseAsModerator: this.videoAbuseAsModerator, + blacklistOnMyVideo: this.blacklistOnMyVideo + } + } +} diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts new file mode 100644 index 000000000..e22f0d57f --- /dev/null +++ b/server/models/account/user-notification.ts @@ -0,0 +1,256 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { UserNotification, UserNotificationType } from '../../../shared' +import { getSort, throwIfNotValid } from '../utils' +import { isBooleanValid } from '../../helpers/custom-validators/misc' +import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' +import { UserModel } from './user' +import { VideoModel } from '../video/video' +import { VideoCommentModel } from '../video/video-comment' +import { Op } from 'sequelize' +import { VideoChannelModel } from '../video/video-channel' +import { AccountModel } from './account' +import { VideoAbuseModel } from '../video/video-abuse' +import { VideoBlacklistModel } from '../video/video-blacklist' + +enum ScopeNames { + WITH_ALL = 'WITH_ALL' +} + +@Scopes({ + [ScopeNames.WITH_ALL]: { + include: [ + { + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'name' ], + model: () => VideoChannelModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoCommentModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped() + }, + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoAbuseModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + }, + { + attributes: [ 'id' ], + model: () => VideoBlacklistModel.unscoped(), + required: false, + include: [ + { + required: true, + attributes: [ 'id', 'uuid', 'name' ], + model: () => VideoModel.unscoped() + } + ] + } + ] + } +}) +@Table({ + tableName: 'userNotification', + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'commentId' ] + } + ] +}) +export class UserNotificationModel extends Model { + + @AllowNull(false) + @Default(null) + @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) + @Column + type: UserNotificationType + + @AllowNull(false) + @Default(false) + @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) + @Column + read: boolean + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Video: VideoModel + + @ForeignKey(() => VideoCommentModel) + @Column + commentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Comment: VideoCommentModel + + @ForeignKey(() => VideoAbuseModel) + @Column + videoAbuseId: number + + @BelongsTo(() => VideoAbuseModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoAbuse: VideoAbuseModel + + @ForeignKey(() => VideoBlacklistModel) + @Column + videoBlacklistId: number + + @BelongsTo(() => VideoBlacklistModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoBlacklist: VideoBlacklistModel + + static listForApi (userId: number, start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: getSort(sort), + where: { + userId + } + } + + return UserNotificationModel.scope(ScopeNames.WITH_ALL) + .findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static markAsRead (userId: number, notificationIds: number[]) { + const query = { + where: { + userId, + id: { + [Op.any]: notificationIds + } + } + } + + return UserNotificationModel.update({ read: true }, query) + } + + toFormattedJSON (): UserNotification { + const video = this.Video ? { + id: this.Video.id, + uuid: this.Video.uuid, + name: this.Video.name, + channel: { + id: this.Video.VideoChannel.id, + displayName: this.Video.VideoChannel.getDisplayName() + } + } : undefined + + const comment = this.Comment ? { + id: this.Comment.id, + account: { + id: this.Comment.Account.id, + displayName: this.Comment.Account.getDisplayName() + }, + video: { + id: this.Comment.Video.id, + uuid: this.Comment.Video.uuid, + name: this.Comment.Video.name + } + } : undefined + + const videoAbuse = this.VideoAbuse ? { + id: this.VideoAbuse.id, + video: { + id: this.VideoAbuse.Video.id, + uuid: this.VideoAbuse.Video.uuid, + name: this.VideoAbuse.Video.name + } + } : undefined + + const videoBlacklist = this.VideoBlacklist ? { + id: this.VideoBlacklist.id, + video: { + id: this.VideoBlacklist.Video.id, + uuid: this.VideoBlacklist.Video.uuid, + name: this.VideoBlacklist.Video.name + } + } : undefined + + return { + id: this.id, + type: this.type, + read: this.read, + video, + comment, + videoAbuse, + videoBlacklist, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString() + } + } +} diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 180ced810..55ec14d05 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -32,8 +32,8 @@ import { isUserUsernameValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid, - isUserWebTorrentEnabledValid, - isUserVideosHistoryEnabledValid + isUserVideosHistoryEnabledValid, + isUserWebTorrentEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' import { OAuthTokenModel } from '../oauth/oauth-token' @@ -44,6 +44,10 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { values } from 'lodash' import { NSFW_POLICY_TYPES } from '../../initializers' import { clearCacheByUserId } from '../../lib/oauth-model' +import { UserNotificationSettingModel } from './user-notification-setting' +import { VideoModel } from '../video/video' +import { ActorModel } from '../activitypub/actor' +import { ActorFollowModel } from '../activitypub/actor-follow' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -54,6 +58,10 @@ enum ScopeNames { { model: () => AccountModel, required: true + }, + { + model: () => UserNotificationSettingModel, + required: true } ] }) @@ -64,6 +72,10 @@ enum ScopeNames { model: () => AccountModel, required: true, include: [ () => VideoChannelModel ] + }, + { + model: () => UserNotificationSettingModel, + required: true } ] } @@ -167,6 +179,13 @@ export class UserModel extends Model { }) Account: AccountModel + @HasOne(() => UserNotificationSettingModel, { + foreignKey: 'userId', + onDelete: 'cascade', + hooks: true + }) + NotificationSetting: UserNotificationSettingModel + @HasMany(() => OAuthTokenModel, { foreignKey: 'userId', onDelete: 'cascade' @@ -249,13 +268,12 @@ export class UserModel extends Model { }) } - static listEmailsWithRight (right: UserRight) { + static listWithRight (right: UserRight) { const roles = Object.keys(USER_ROLE_LABELS) .map(k => parseInt(k, 10) as UserRole) .filter(role => hasUserRight(role, right)) const query = { - attribute: [ 'email' ], where: { role: { [Sequelize.Op.in]: roles @@ -263,9 +281,46 @@ export class UserModel extends Model { } } - return UserModel.unscoped() - .findAll(query) - .then(u => u.map(u => u.email)) + return UserModel.findAll(query) + } + + static listUserSubscribersOf (actorId: number) { + const query = { + include: [ + { + model: UserNotificationSettingModel.unscoped(), + required: true + }, + { + attributes: [ 'userId' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ ], + model: ActorModel.unscoped(), + required: true, + where: { + serverId: null + }, + include: [ + { + attributes: [ ], + as: 'ActorFollowings', + model: ActorFollowModel.unscoped(), + required: true, + where: { + targetActorId: actorId + } + } + ] + } + ] + } + ] + } + + return UserModel.unscoped().findAll(query) } static loadById (id: number) { @@ -314,6 +369,37 @@ export class UserModel extends Model { return UserModel.findOne(query) } + static loadByVideoId (videoId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() @@ -380,6 +466,7 @@ export class UserModel extends Model { blocked: this.blocked, blockedReason: this.blockedReason, account: this.Account.toFormattedJSON(), + notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, videoChannels: [], videoQuotaUsed: videoQuotaUsed !== undefined ? parseInt(videoQuotaUsed, 10) diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 994f791de..796e07a42 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -307,7 +307,7 @@ export class ActorFollowModel extends Model { }) } - static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { + static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { const query = { distinct: true, offset: start, @@ -335,7 +335,7 @@ export class ActorFollowModel extends Model { as: 'ActorFollowing', required: true, where: { - id + id: actorId } } ] @@ -350,7 +350,7 @@ export class ActorFollowModel extends Model { }) } - static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { + static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { const query = { attributes: [], distinct: true, @@ -358,7 +358,7 @@ export class ActorFollowModel extends Model { limit: count, order: getSort(sort), where: { - actorId: id + actorId: actorId }, include: [ { @@ -451,9 +451,9 @@ export class ActorFollowModel extends Model { static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + 'WHERE id IN (' + - 'SELECT "actorFollow"."id" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + - `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + + `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + ')' const options = { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 12b83916e..dda57a8ba 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -219,6 +219,7 @@ export class ActorModel extends Model { name: 'actorId', allowNull: false }, + as: 'ActorFollowings', onDelete: 'cascade' }) ActorFollowing: ActorFollowModel[] diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..4c9e2d05e 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -86,11 +86,6 @@ export class VideoAbuseModel extends Model { }) Video: VideoModel - @AfterCreate - static sendEmailNotification (instance: VideoAbuseModel) { - return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) - } - static loadByIdAndVideoId (id: number, videoId: number) { const query = { where: { diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..23e992685 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -53,16 +53,6 @@ export class VideoBlacklistModel extends Model { }) Video: VideoModel - @AfterCreate - static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { - return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) - } - - @AfterDestroy - static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { - return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) - } - static listForApi (start: number, count: number, sort: SortType) { const query = { offset: start, diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index dd6d08139..d8fc2a564 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -448,6 +448,10 @@ export class VideoCommentModel extends Model { } } + getCommentStaticPath () { + return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() + } + getThreadId (): number { return this.originCommentId || this.id } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index bcf327f32..fc200e5d1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1527,6 +1527,10 @@ export class VideoModel extends Model { videoFile.infoHash = parsedTorrent.infoHash } + getWatchStaticPath () { + return '/videos/watch/' + this.uuid + } + getEmbedStaticPath () { return '/videos/embed/' + this.uuid } -- cgit v1.2.3 From dc13348070d808d0ba3feb56a435b835c2e7e791 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 2 Jan 2019 16:37:43 +0100 Subject: Add import finished and video published notifs --- server/models/account/account-blocklist.ts | 15 +++ server/models/account/user-notification-setting.ts | 22 ++- server/models/account/user-notification.ts | 150 +++++++++++++-------- server/models/account/user.ts | 24 ++++ server/models/video/video-file.ts | 2 - server/models/video/video-import.ts | 4 + server/models/video/video.ts | 10 ++ 7 files changed, 165 insertions(+), 62 deletions(-) (limited to 'server/models') diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fa2819235..54ac290c4 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -72,6 +72,21 @@ export class AccountBlocklistModel extends Model { }) BlockedAccount: AccountModel + static isAccountMutedBy (accountId: number, targetAccountId: number) { + const query = { + attributes: [ 'id' ], + where: { + accountId, + targetAccountId + }, + raw: true + } + + return AccountBlocklistModel.unscoped() + .findOne(query) + .then(a => !!a) + } + static loadByAccountAndTarget (accountId: number, targetAccountId: number) { const query = { where: { diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index bc24b1e33..6470defa7 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts @@ -65,6 +65,24 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') + ) + @Column + myVideoPublished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoImportFinished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') + ) + @Column + myVideoImportFinished: UserNotificationSettingValue + @ForeignKey(() => UserModel) @Column userId: number @@ -94,7 +112,9 @@ export class UserNotificationSettingModel extends Model VideoModel.unscoped(), + required + } +} + +function buildChannelInclude () { + return { + required: true, + attributes: [ 'id', 'name' ], + model: () => VideoChannelModel.unscoped() + } +} + +function buildAccountInclude () { + return { + required: true, + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped() + } +} + @Scopes({ [ScopeNames.WITH_ALL]: { include: [ + Object.assign(buildVideoInclude(false), { + include: [ buildChannelInclude() ] + }), { - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped(), - required: false, - include: [ - { - required: true, - attributes: [ 'id', 'name' ], - model: () => VideoChannelModel.unscoped() - } - ] - }, - { - attributes: [ 'id' ], + attributes: [ 'id', 'originCommentId' ], model: () => VideoCommentModel.unscoped(), required: false, include: [ - { - required: true, - attributes: [ 'id', 'name' ], - model: () => AccountModel.unscoped() - }, - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } + buildAccountInclude(), + buildVideoInclude(true) ] }, { attributes: [ 'id' ], model: () => VideoAbuseModel.unscoped(), required: false, - include: [ - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } - ] + include: [ buildVideoInclude(true) ] }, { attributes: [ 'id' ], model: () => VideoBlacklistModel.unscoped(), required: false, - include: [ - { - required: true, - attributes: [ 'id', 'uuid', 'name' ], - model: () => VideoModel.unscoped() - } - ] + include: [ buildVideoInclude(true) ] + }, + { + attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], + model: () => VideoImportModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] } ] } @@ -166,8 +181,20 @@ export class UserNotificationModel extends Model { }) VideoBlacklist: VideoBlacklistModel - static listForApi (userId: number, start: number, count: number, sort: string) { - const query = { + @ForeignKey(() => VideoImportModel) + @Column + videoImportId: number + + @BelongsTo(() => VideoImportModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoImport: VideoImportModel + + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { + const query: IFindOptions = { offset: start, limit: count, order: getSort(sort), @@ -176,6 +203,8 @@ export class UserNotificationModel extends Model { } } + if (unread !== undefined) query.where['read'] = !unread + return UserNotificationModel.scope(ScopeNames.WITH_ALL) .findAndCountAll(query) .then(({ rows, count }) => { @@ -200,45 +229,39 @@ export class UserNotificationModel extends Model { } toFormattedJSON (): UserNotification { - const video = this.Video ? { - id: this.Video.id, - uuid: this.Video.uuid, - name: this.Video.name, + const video = this.Video ? Object.assign(this.formatVideo(this.Video), { channel: { id: this.Video.VideoChannel.id, displayName: this.Video.VideoChannel.getDisplayName() } + }) : undefined + + const videoImport = this.VideoImport ? { + id: this.VideoImport.id, + video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, + torrentName: this.VideoImport.torrentName, + magnetUri: this.VideoImport.magnetUri, + targetUrl: this.VideoImport.targetUrl } : undefined const comment = this.Comment ? { id: this.Comment.id, + threadId: this.Comment.getThreadId(), account: { id: this.Comment.Account.id, displayName: this.Comment.Account.getDisplayName() }, - video: { - id: this.Comment.Video.id, - uuid: this.Comment.Video.uuid, - name: this.Comment.Video.name - } + video: this.formatVideo(this.Comment.Video) } : undefined const videoAbuse = this.VideoAbuse ? { id: this.VideoAbuse.id, - video: { - id: this.VideoAbuse.Video.id, - uuid: this.VideoAbuse.Video.uuid, - name: this.VideoAbuse.Video.name - } + video: this.formatVideo(this.VideoAbuse.Video) } : undefined const videoBlacklist = this.VideoBlacklist ? { id: this.VideoBlacklist.id, - video: { - id: this.VideoBlacklist.Video.id, - uuid: this.VideoBlacklist.Video.uuid, - name: this.VideoBlacklist.Video.name - } + video: this.formatVideo(this.VideoBlacklist.Video) } : undefined return { @@ -246,6 +269,7 @@ export class UserNotificationModel extends Model { type: this.type, read: this.read, video, + videoImport, comment, videoAbuse, videoBlacklist, @@ -253,4 +277,12 @@ export class UserNotificationModel extends Model { updatedAt: this.updatedAt.toISOString() } } + + private formatVideo (video: VideoModel) { + return { + id: video.id, + uuid: video.uuid, + name: video.name + } + } } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 55ec14d05..33f56f641 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -48,6 +48,7 @@ import { UserNotificationSettingModel } from './user-notification-setting' import { VideoModel } from '../video/video' import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' +import { VideoImportModel } from '../video/video-import' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -186,6 +187,12 @@ export class UserModel extends Model { }) NotificationSetting: UserNotificationSettingModel + @HasMany(() => VideoImportModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + VideoImports: VideoImportModel[] + @HasMany(() => OAuthTokenModel, { foreignKey: 'userId', onDelete: 'cascade' @@ -400,6 +407,23 @@ export class UserModel extends Model { return UserModel.findOne(query) } + static loadByVideoImportId (videoImportId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoImportModel.unscoped(), + where: { + id: videoImportId + } + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 3fd2d5a99..0fd868cd6 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -1,4 +1,3 @@ -import { values } from 'lodash' import { AllowNull, BelongsTo, @@ -20,7 +19,6 @@ import { isVideoFileSizeValid, isVideoFPSResolutionValid } from '../../helpers/custom-validators/videos' -import { CONSTRAINTS_FIELDS } from '../../initializers' import { throwIfNotValid } from '../utils' import { VideoModel } from './video' import * as Sequelize from 'sequelize' diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8d442b3f8..c723e57c0 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts @@ -144,6 +144,10 @@ export class VideoImportModel extends Model { }) } + getTargetIdentifier () { + return this.targetUrl || this.magnetUri || this.torrentName + } + toFormattedJSON (): VideoImport { const videoFormatOptions = { completeDescription: true, diff --git a/server/models/video/video.ts b/server/models/video/video.ts index fc200e5d1..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -94,6 +94,7 @@ import { import * as validator from 'validator' import { UserVideoHistoryModel } from '../account/user-video-history' import { UserModel } from '../account/user' +import { VideoImportModel } from './video-import' // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -785,6 +786,15 @@ export class VideoModel extends Model { }) VideoBlacklist: VideoBlacklistModel + @HasOne(() => VideoImportModel, { + foreignKey: { + name: 'videoId', + allowNull: true + }, + onDelete: 'set null' + }) + VideoImport: VideoImportModel + @HasMany(() => VideoCaptionModel, { foreignKey: { name: 'videoId', -- cgit v1.2.3 From f7cc67b455a12ccae9b0ea16876d166720364357 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 4 Jan 2019 08:56:20 +0100 Subject: Add new follow, mention and user registered notifs --- server/models/account/account-blocklist.ts | 24 +++++- server/models/account/user-notification-setting.ts | 32 +++++++- server/models/account/user-notification.ts | 95 ++++++++++++++++++++-- server/models/account/user.ts | 51 ++++++++++++ server/models/video/video-comment.ts | 41 +++++++++- 5 files changed, 228 insertions(+), 15 deletions(-) (limited to 'server/models') diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 54ac290c4..efd6ed59e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated import { AccountModel } from './account' import { getSort } from '../utils' import { AccountBlock } from '../../../shared/models/blocklist' +import { Op } from 'sequelize' enum ScopeNames { WITH_ACCOUNTS = 'WITH_ACCOUNTS' @@ -73,18 +74,33 @@ export class AccountBlocklistModel extends Model { BlockedAccount: AccountModel static isAccountMutedBy (accountId: number, targetAccountId: number) { + return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId) + .then(result => result[accountId]) + } + + static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { const query = { - attributes: [ 'id' ], + attributes: [ 'accountId', 'id' ], where: { - accountId, + accountId: { + [Op.any]: accountIds + }, targetAccountId }, raw: true } return AccountBlocklistModel.unscoped() - .findOne(query) - .then(a => !!a) + .findAll(query) + .then(rows => { + const result: { [accountId: number]: boolean } = {} + + for (const accountId of accountIds) { + result[accountId] = !!rows.find(r => r.accountId === accountId) + } + + return result + }) } static loadByAccountAndTarget (accountId: number, targetAccountId: number) { diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index 6470defa7..f1c3ac223 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts @@ -83,6 +83,33 @@ export class UserNotificationSettingModel extends Model throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') + ) + @Column + newUserRegistration: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewFollow', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') + ) + @Column + newFollow: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingCommentMention', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') + ) + @Column + commentMention: UserNotificationSettingValue + @ForeignKey(() => UserModel) @Column userId: number @@ -114,7 +141,10 @@ export class UserNotificationSettingModel extends Model VideoChannelModel.unscoped() } } -function buildAccountInclude () { +function buildAccountInclude (required: boolean) { return { - required: true, + required, attributes: [ 'id', 'name' ], model: () => AccountModel.unscoped() } @@ -58,14 +60,14 @@ function buildAccountInclude () { [ScopeNames.WITH_ALL]: { include: [ Object.assign(buildVideoInclude(false), { - include: [ buildChannelInclude() ] + include: [ buildChannelInclude(true) ] }), { attributes: [ 'id', 'originCommentId' ], model: () => VideoCommentModel.unscoped(), required: false, include: [ - buildAccountInclude(), + buildAccountInclude(true), buildVideoInclude(true) ] }, @@ -86,6 +88,42 @@ function buildAccountInclude () { model: () => VideoImportModel.unscoped(), required: false, include: [ buildVideoInclude(false) ] + }, + { + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true + } + ] + }, + { + attributes: [ 'id' ], + model: () => ActorFollowModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true, + as: 'ActorFollower', + include: [ buildAccountInclude(true) ] + }, + { + attributes: [ 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + include: [ + buildChannelInclude(false), + buildAccountInclude(false) + ] + } + ] } ] } @@ -193,6 +231,30 @@ export class UserNotificationModel extends Model { }) VideoImport: VideoImportModel + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Account: AccountModel + + @ForeignKey(() => ActorFollowModel) + @Column + actorFollowId: number + + @BelongsTo(() => ActorFollowModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + ActorFollow: ActorFollowModel + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { const query: IFindOptions = { offset: start, @@ -264,6 +326,25 @@ export class UserNotificationModel extends Model { video: this.formatVideo(this.VideoBlacklist.Video) } : undefined + const account = this.Account ? { + id: this.Account.id, + displayName: this.Account.getDisplayName(), + name: this.Account.Actor.preferredUsername + } : undefined + + const actorFollow = this.ActorFollow ? { + id: this.ActorFollow.id, + follower: { + displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), + name: this.ActorFollow.ActorFollower.preferredUsername + }, + following: { + type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', + displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), + name: this.ActorFollow.ActorFollowing.preferredUsername + } + } : undefined + return { id: this.id, type: this.type, @@ -273,6 +354,8 @@ export class UserNotificationModel extends Model { comment, videoAbuse, videoBlacklist, + account, + actorFollow, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString() } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 33f56f641..017a96657 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -330,6 +330,16 @@ export class UserModel extends Model { return UserModel.unscoped().findAll(query) } + static listByUsernames (usernames: string[]) { + const query = { + where: { + username: usernames + } + } + + return UserModel.findAll(query) + } + static loadById (id: number) { return UserModel.findById(id) } @@ -424,6 +434,47 @@ export class UserModel extends Model { return UserModel.findOne(query) } + static loadByChannelActorId (videoChannelActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + where: { + actorId: videoChannelActorId + } + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByAccountActorId (accountActorId: number) { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + where: { + actorId: accountActorId + } + } + ] + } + + return UserModel.findOne(query) + } + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = UserModel.generateUserQuotaBaseSQL() diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index d8fc2a564..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' import { VideoComment } from '../../../shared/models/videos/video-comment.model' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONSTRAINTS_FIELDS } from '../../initializers' +import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { sendDeleteVideoComment } from '../../lib/activitypub/send' import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor' @@ -29,6 +29,9 @@ import { VideoModel } from './video' import { VideoChannelModel } from './video-channel' import { getServerActor } from '../../helpers/utils' import { UserModel } from '../account/user' +import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' +import { regexpCapture } from '../../helpers/regexp' +import { uniq } from 'lodash' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', @@ -370,9 +373,11 @@ export class VideoCommentModel extends Model { id: { [ Sequelize.Op.in ]: Sequelize.literal('(' + 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + - 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + - 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + - 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + + `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + + 'UNION ' + + 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + + 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + + ') ' + 'SELECT id FROM children' + ')'), [ Sequelize.Op.ne ]: comment.id @@ -460,6 +465,34 @@ export class VideoCommentModel extends Model { return this.Account.isOwned() } + extractMentions () { + if (!this.text) return [] + + const localMention = `@(${actorNameAlphabet}+)` + const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` + + const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') + const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') + const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') + const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') + + return uniq( + [].concat( + regexpCapture(this.text, remoteMentionsRegex) + .map(([ , username ]) => username), + + regexpCapture(this.text, localMentionsRegex) + .map(([ , username ]) => username), + + regexpCapture(this.text, firstMentionRegex) + .map(([ , username1, username2 ]) => username1 || username2), + + regexpCapture(this.text, endMentionRegex) + .map(([ , username1, username2 ]) => username1 || username2) + ) + ) + } + toFormattedJSON () { return { id: this.id, -- cgit v1.2.3 From 2f1548fda32c3ba9e53913270394eedfacd55986 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 8 Jan 2019 11:26:41 +0100 Subject: Add notifications in the client --- server/models/account/user-notification.ts | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'server/models') diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 79afce600..9e4f982a3 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -290,6 +290,12 @@ export class UserNotificationModel extends Model { return UserNotificationModel.update({ read: true }, query) } + static markAllAsRead (userId: number) { + const query = { where: { userId } } + + return UserNotificationModel.update({ read: true }, query) + } + toFormattedJSON (): UserNotification { const video = this.Video ? Object.assign(this.formatVideo(this.Video), { channel: { -- cgit v1.2.3 From 5abb9fbbd12e7097e348d6a38622d364b1fa47ed Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 10 Jan 2019 15:39:51 +0100 Subject: Add ability to unfederate a local video (on blacklist) --- server/models/video/video-blacklist.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) (limited to 'server/models') diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 23e992685..3b567e488 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -1,21 +1,7 @@ -import { - AfterCreate, - AfterDestroy, - AllowNull, - BelongsTo, - Column, - CreatedAt, - DataType, - ForeignKey, - Is, - Model, - Table, - UpdatedAt -} from 'sequelize-typescript' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { getSortOnModel, SortType, throwIfNotValid } from '../utils' import { VideoModel } from './video' import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' -import { Emailer } from '../../lib/emailer' import { VideoBlacklist } from '../../../shared/models/videos' import { CONSTRAINTS_FIELDS } from '../../initializers' @@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model { @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) reason: string + @AllowNull(false) + @Column + unfederated: boolean + @CreatedAt createdAt: Date @@ -93,6 +83,7 @@ export class VideoBlacklistModel extends Model { createdAt: this.createdAt, updatedAt: this.updatedAt, reason: this.reason, + unfederated: this.unfederated, video: { id: video.id, -- cgit v1.2.3 From 744d0eca195bce7dafeb4a958d0eb3c0046be32d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Jan 2019 11:30:15 +0100 Subject: Refresh remote actors on GET enpoints --- server/models/account/account.ts | 4 ++++ server/models/video/video-channel.ts | 4 ++++ 2 files changed, 8 insertions(+) (limited to 'server/models') diff --git a/server/models/account/account.ts b/server/models/account/account.ts index a99e9b1ad..84ef0b30d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -288,6 +288,10 @@ export class AccountModel extends Model { return this.Actor.isOwned() } + isOutdated () { + return this.Actor.isOutdated() + } + getDisplayName () { return this.name } diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 86bf0461a..5598d80f6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -470,4 +470,8 @@ export class VideoChannelModel extends Model { getDisplayName () { return this.name } + + isOutdated () { + return this.Actor.isOutdated() + } } -- cgit v1.2.3 From 1506307f2f903ce0f80155072a33345c702b7c76 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Jan 2019 16:48:38 +0100 Subject: Increase abuse length to 3000 And correctly handle new lines --- server/models/video/video-abuse.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) (limited to 'server/models') diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 4c9e2d05e..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -1,17 +1,4 @@ -import { - AfterCreate, - AllowNull, - BelongsTo, - Column, - CreatedAt, - DataType, - Default, - ForeignKey, - Is, - Model, - Table, - UpdatedAt -} from 'sequelize-typescript' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuse } from '../../../shared/models/videos' import { @@ -19,7 +6,6 @@ import { isVideoAbuseReasonValid, isVideoAbuseStateValid } from '../../helpers/custom-validators/video-abuses' -import { Emailer } from '../../lib/emailer' import { AccountModel } from '../account/account' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' @@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' export class VideoAbuseModel extends Model { @AllowNull(false) + @Default(null) @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) - @Column + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) reason: string @AllowNull(false) -- cgit v1.2.3 From 44b9c0ba31c4a97e3d874f33226ad935c3a90dd5 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 15 Jan 2019 09:45:54 +0100 Subject: Add totalLocalVideoFilesSize in stats --- server/models/redundancy/video-redundancy.ts | 2 +- server/models/video/video-file.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'server/models') diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8b6cd146a..8f2ef2d9a 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -395,7 +395,7 @@ export class VideoRedundancyModel extends Model { ] } - return VideoRedundancyModel.find(query as any) // FIXME: typings + return VideoRedundancyModel.findOne(query as any) // FIXME: typings .then((r: any) => ({ totalUsed: parseInt(r.totalUsed.toString(), 10), totalVideos: r.totalVideos, diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 0fd868cd6..1f1b76c1e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -120,6 +120,26 @@ export class VideoFileModel extends Model { return VideoFileModel.findById(id, options) } + static async getStats () { + let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { + include: [ + { + attributes: [], + model: VideoModel.unscoped(), + where: { + remote: false + } + } + ] + } as any) + // Sequelize could return null... + if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0 + + return { + totalLocalVideoFilesSize + } + } + hasSameUniqueKeysThan (other: VideoFileModel) { return this.fps === other.fps && this.resolution === other.resolution && -- cgit v1.2.3 From 457bb213b273a9b206cc5654eb085cede4e916ad Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 16 Jan 2019 16:05:40 +0100 Subject: Refactor how we use icons Inject them in an angular component so we can easily change their color --- server/models/account/user-notification.ts | 158 ++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 38 deletions(-) (limited to 'server/models') diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 9e4f982a3..1094eec78 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -27,11 +27,27 @@ import { VideoBlacklistModel } from '../video/video-blacklist' import { VideoImportModel } from '../video/video-import' import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' +import { AvatarModel } from '../avatar/avatar' enum ScopeNames { WITH_ALL = 'WITH_ALL' } +function buildActorWithAvatarInclude () { + return { + attributes: [ 'preferredUsername' ], + model: () => ActorModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'filename' ], + model: () => AvatarModel.unscoped(), + required: false + } + ] + } +} + function buildVideoInclude (required: boolean) { return { attributes: [ 'id', 'uuid', 'name' ], @@ -40,19 +56,21 @@ function buildVideoInclude (required: boolean) { } } -function buildChannelInclude (required: boolean) { +function buildChannelInclude (required: boolean, withActor = false) { return { required, attributes: [ 'id', 'name' ], - model: () => VideoChannelModel.unscoped() + model: () => VideoChannelModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] } } -function buildAccountInclude (required: boolean) { +function buildAccountInclude (required: boolean, withActor = false) { return { required, attributes: [ 'id', 'name' ], - model: () => AccountModel.unscoped() + model: () => AccountModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] } } @@ -60,47 +78,40 @@ function buildAccountInclude (required: boolean) { [ScopeNames.WITH_ALL]: { include: [ Object.assign(buildVideoInclude(false), { - include: [ buildChannelInclude(true) ] + include: [ buildChannelInclude(true, true) ] }), + { attributes: [ 'id', 'originCommentId' ], model: () => VideoCommentModel.unscoped(), required: false, include: [ - buildAccountInclude(true), + buildAccountInclude(true, true), buildVideoInclude(true) ] }, + { attributes: [ 'id' ], model: () => VideoAbuseModel.unscoped(), required: false, include: [ buildVideoInclude(true) ] }, + { attributes: [ 'id' ], model: () => VideoBlacklistModel.unscoped(), required: false, include: [ buildVideoInclude(true) ] }, + { attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], model: () => VideoImportModel.unscoped(), required: false, include: [ buildVideoInclude(false) ] }, - { - attributes: [ 'id', 'name' ], - model: () => AccountModel.unscoped(), - required: false, - include: [ - { - attributes: [ 'id', 'preferredUsername' ], - model: () => ActorModel.unscoped(), - required: true - } - ] - }, + { attributes: [ 'id' ], model: () => ActorFollowModel.unscoped(), @@ -111,7 +122,18 @@ function buildAccountInclude (required: boolean) { model: () => ActorModel.unscoped(), required: true, as: 'ActorFollower', - include: [ buildAccountInclude(true) ] + include: [ + { + attributes: [ 'id', 'name' ], + model: () => AccountModel.unscoped(), + required: true + }, + { + attributes: [ 'filename' ], + model: () => AvatarModel.unscoped(), + required: false + } + ] }, { attributes: [ 'preferredUsername' ], @@ -124,7 +146,9 @@ function buildAccountInclude (required: boolean) { ] } ] - } + }, + + buildAccountInclude(false, true) ] } }) @@ -132,10 +156,63 @@ function buildAccountInclude (required: boolean) { tableName: 'userNotification', indexes: [ { - fields: [ 'videoId' ] + fields: [ 'userId' ] }, { - fields: [ 'commentId' ] + fields: [ 'videoId' ], + where: { + videoId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'commentId' ], + where: { + commentId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoAbuseId' ], + where: { + videoAbuseId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoBlacklistId' ], + where: { + videoBlacklistId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoImportId' ], + where: { + videoImportId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'accountId' ], + where: { + accountId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'actorFollowId' ], + where: { + actorFollowId: { + [Op.ne]: null + } + } } ] }) @@ -297,12 +374,9 @@ export class UserNotificationModel extends Model { } toFormattedJSON (): UserNotification { - const video = this.Video ? Object.assign(this.formatVideo(this.Video), { - channel: { - id: this.Video.VideoChannel.id, - displayName: this.Video.VideoChannel.getDisplayName() - } - }) : undefined + const video = this.Video + ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) + : undefined const videoImport = this.VideoImport ? { id: this.VideoImport.id, @@ -315,10 +389,7 @@ export class UserNotificationModel extends Model { const comment = this.Comment ? { id: this.Comment.id, threadId: this.Comment.getThreadId(), - account: { - id: this.Comment.Account.id, - displayName: this.Comment.Account.getDisplayName() - }, + account: this.formatActor(this.Comment.Account), video: this.formatVideo(this.Comment.Video) } : undefined @@ -332,17 +403,15 @@ export class UserNotificationModel extends Model { video: this.formatVideo(this.VideoBlacklist.Video) } : undefined - const account = this.Account ? { - id: this.Account.id, - displayName: this.Account.getDisplayName(), - name: this.Account.Actor.preferredUsername - } : undefined + const account = this.Account ? this.formatActor(this.Account) : undefined const actorFollow = this.ActorFollow ? { id: this.ActorFollow.id, follower: { + id: this.ActorFollow.ActorFollower.Account.id, displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), - name: this.ActorFollow.ActorFollower.preferredUsername + name: this.ActorFollow.ActorFollower.preferredUsername, + avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined }, following: { type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', @@ -374,4 +443,17 @@ export class UserNotificationModel extends Model { name: video.name } } + + private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { + const avatar = accountOrChannel.Actor.Avatar + ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() } + : undefined + + return { + id: accountOrChannel.id, + displayName: accountOrChannel.getDisplayName(), + name: accountOrChannel.Actor.preferredUsername, + avatar + } + } } -- cgit v1.2.3 From 38967f7b73cec6f6198c72d62f8d64bb88e6951c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 21 Jan 2019 13:52:46 +0100 Subject: Add server host in notification account field --- server/models/account/user-notification.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'server/models') diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 1094eec78..6cdbb827b 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -28,6 +28,7 @@ import { VideoImportModel } from '../video/video-import' import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' import { AvatarModel } from '../avatar/avatar' +import { ServerModel } from '../server/server' enum ScopeNames { WITH_ALL = 'WITH_ALL' @@ -43,6 +44,11 @@ function buildActorWithAvatarInclude () { attributes: [ 'filename' ], model: () => AvatarModel.unscoped(), required: false + }, + { + attributes: [ 'host' ], + model: () => ServerModel.unscoped(), + required: false } ] } @@ -132,6 +138,11 @@ function buildAccountInclude (required: boolean, withActor = false) { attributes: [ 'filename' ], model: () => AvatarModel.unscoped(), required: false + }, + { + attributes: [ 'host' ], + model: () => ServerModel.unscoped(), + required: false } ] }, @@ -411,7 +422,8 @@ export class UserNotificationModel extends Model { id: this.ActorFollow.ActorFollower.Account.id, displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), name: this.ActorFollow.ActorFollower.preferredUsername, - avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined + avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined, + host: this.ActorFollow.ActorFollower.getHost() }, following: { type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', @@ -453,6 +465,7 @@ export class UserNotificationModel extends Model { id: accountOrChannel.id, displayName: accountOrChannel.getDisplayName(), name: accountOrChannel.Actor.preferredUsername, + host: accountOrChannel.Actor.getHost(), avatar } } -- cgit v1.2.3