diff options
Diffstat (limited to 'server/models')
51 files changed, 1447 insertions, 889 deletions
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 20008768b..14a5bffa2 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -5,7 +5,7 @@ import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | |||
5 | import { AbuseMessage } from '@shared/models' | 5 | import { AbuseMessage } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 6 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
8 | import { getSort, throwIfNotValid } from '../utils' | 8 | import { getSort, throwIfNotValid } from '../shared' |
9 | import { AbuseModel } from './abuse' | 9 | import { AbuseModel } from './abuse' |
10 | 10 | ||
11 | @Table({ | 11 | @Table({ |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 4c6a96a86..4ce40bf2f 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -34,13 +34,13 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' | 35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' |
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
37 | import { getSort, throwIfNotValid } from '../utils' | 37 | import { getSort, throwIfNotValid } from '../shared' |
38 | import { ThumbnailModel } from '../video/thumbnail' | 38 | import { ThumbnailModel } from '../video/thumbnail' |
39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' | 39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' |
40 | import { VideoBlacklistModel } from '../video/video-blacklist' | 40 | import { VideoBlacklistModel } from '../video/video-blacklist' |
41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | 41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' | 42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' |
43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | 43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './sql/abuse-query-builder' |
44 | import { VideoAbuseModel } from './video-abuse' | 44 | import { VideoAbuseModel } from './video-abuse' |
45 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 45 | import { VideoCommentAbuseModel } from './video-comment-abuse' |
46 | 46 | ||
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/sql/abuse-query-builder.ts index 74f4542e5..282d4541a 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/sql/abuse-query-builder.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | 2 | import { exists } from '@server/helpers/custom-validators/misc' |
3 | import { forceNumber } from '@shared/core-utils' | 3 | import { forceNumber } from '@shared/core-utils' |
4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | 4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' |
5 | import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' | 5 | import { buildBlockedAccountSQL, buildSortDirectionAndField } from '../../shared' |
6 | 6 | ||
7 | export type BuildAbusesQueryOptions = { | 7 | export type BuildAbusesQueryOptions = { |
8 | start: number | 8 | start: number |
@@ -157,7 +157,7 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | | |||
157 | } | 157 | } |
158 | 158 | ||
159 | function buildAbuseOrder (value: string) { | 159 | function buildAbuseOrder (value: string) { |
160 | const { direction, field } = buildDirectionAndField(value) | 160 | const { direction, field } = buildSortDirectionAndField(value) |
161 | 161 | ||
162 | return `ORDER BY "abuse"."${field}" ${direction}` | 162 | return `ORDER BY "abuse"."${field}" ${direction}` |
163 | } | 163 | } |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 377249b38..f6212ff6e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -6,7 +6,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
8 | import { ServerModel } from '../server/server' | 8 | import { ServerModel } from '../server/server' |
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | @Table({ | 12 | @Table({ |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 7afc907da..9e7ef4394 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -11,7 +11,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | 12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' |
13 | import { ActorModel } from '../actor/actor' | 13 | import { ActorModel } from '../actor/actor' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | import { VideoModel } from '../video/video' | 15 | import { VideoModel } from '../video/video' |
16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
17 | import { AccountModel } from './account' | 17 | import { AccountModel } from './account' |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 8a7dfba94..dc989417b 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ModelCache } from '@server/models/model-cache' | 19 | import { ModelCache } from '@server/models/shared/model-cache' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { Account, AccountSummary } from '../../../shared/models/actors' | 21 | import { Account, AccountSummary } from '../../../shared/models/actors' |
22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' | 22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' |
@@ -38,7 +38,7 @@ import { ApplicationModel } from '../application/application' | |||
38 | import { ServerModel } from '../server/server' | 38 | import { ServerModel } from '../server/server' |
39 | import { ServerBlocklistModel } from '../server/server-blocklist' | 39 | import { ServerBlocklistModel } from '../server/server-blocklist' |
40 | import { UserModel } from '../user/user' | 40 | import { UserModel } from '../user/user' |
41 | import { getSort, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared' |
42 | import { VideoModel } from '../video/video' | 42 | import { VideoModel } from '../video/video' |
43 | import { VideoChannelModel } from '../video/video-channel' | 43 | import { VideoChannelModel } from '../video/video-channel' |
44 | import { VideoCommentModel } from '../video/video-comment' | 44 | import { VideoCommentModel } from '../video/video-comment' |
@@ -251,6 +251,18 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
251 | return undefined | 251 | return undefined |
252 | } | 252 | } |
253 | 253 | ||
254 | // --------------------------------------------------------------------------- | ||
255 | |||
256 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
257 | return buildSQLAttributes({ | ||
258 | model: this, | ||
259 | tableName, | ||
260 | aliasPrefix | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
254 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { | 266 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { |
255 | return AccountModel.findByPk(id, { transaction }) | 267 | return AccountModel.findByPk(id, { transaction }) |
256 | } | 268 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 9615229dd..32e5d78b0 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -38,7 +38,7 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM | |||
38 | import { AccountModel } from '../account/account' | 38 | import { AccountModel } from '../account/account' |
39 | import { ServerModel } from '../server/server' | 39 | import { ServerModel } from '../server/server' |
40 | import { doesExist } from '../shared/query' | 40 | import { doesExist } from '../shared/query' |
41 | import { createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' |
42 | import { VideoChannelModel } from '../video/video-channel' | 42 | import { VideoChannelModel } from '../video/video-channel' |
43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | 44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' |
@@ -140,6 +140,18 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
140 | }) | 140 | }) |
141 | } | 141 | } |
142 | 142 | ||
143 | // --------------------------------------------------------------------------- | ||
144 | |||
145 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
146 | return buildSQLAttributes({ | ||
147 | model: this, | ||
148 | tableName, | ||
149 | aliasPrefix | ||
150 | }) | ||
151 | } | ||
152 | |||
153 | // --------------------------------------------------------------------------- | ||
154 | |||
143 | /* | 155 | /* |
144 | * @deprecated Use `findOrCreateCustom` instead | 156 | * @deprecated Use `findOrCreateCustom` instead |
145 | */ | 157 | */ |
@@ -213,7 +225,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
213 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + | 225 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + |
214 | `LIMIT 1` | 226 | `LIMIT 1` |
215 | 227 | ||
216 | return doesExist(query, { actorId, followerActorId }) | 228 | return doesExist(this.sequelize, query, { actorId, followerActorId }) |
217 | } | 229 | } |
218 | 230 | ||
219 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 231 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index f2b3b2f4b..9c34a0101 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -22,7 +22,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp | |||
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
25 | import { throwIfNotValid } from '../utils' | 25 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
26 | import { ActorModel } from './actor' | 26 | import { ActorModel } from './actor' |
27 | 27 | ||
28 | @Table({ | 28 | @Table({ |
@@ -94,6 +94,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) | 94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) |
95 | } | 95 | } |
96 | 96 | ||
97 | // --------------------------------------------------------------------------- | ||
98 | |||
99 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
100 | return buildSQLAttributes({ | ||
101 | model: this, | ||
102 | tableName, | ||
103 | aliasPrefix | ||
104 | }) | ||
105 | } | ||
106 | |||
107 | // --------------------------------------------------------------------------- | ||
108 | |||
97 | static loadByName (filename: string) { | 109 | static loadByName (filename: string) { |
98 | const query = { | 110 | const query = { |
99 | where: { | 111 | where: { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index d7afa727d..1432e8757 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { activityPubContextify } from '@server/lib/activitypub/context' | 18 | import { activityPubContextify } from '@server/lib/activitypub/context' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | 19 | import { getBiggestActorImage } from '@server/lib/actor-image' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/shared/model-cache' |
21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' | 21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | 22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' |
23 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -55,7 +55,7 @@ import { | |||
55 | import { AccountModel } from '../account/account' | 55 | import { AccountModel } from '../account/account' |
56 | import { getServerActor } from '../application/application' | 56 | import { getServerActor } from '../application/application' |
57 | import { ServerModel } from '../server/server' | 57 | import { ServerModel } from '../server/server' |
58 | import { isOutdated, throwIfNotValid } from '../utils' | 58 | import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../shared' |
59 | import { VideoModel } from '../video/video' | 59 | import { VideoModel } from '../video/video' |
60 | import { VideoChannelModel } from '../video/video-channel' | 60 | import { VideoChannelModel } from '../video/video-channel' |
61 | import { ActorFollowModel } from './actor-follow' | 61 | import { ActorFollowModel } from './actor-follow' |
@@ -65,7 +65,7 @@ enum ScopeNames { | |||
65 | FULL = 'FULL' | 65 | FULL = 'FULL' |
66 | } | 66 | } |
67 | 67 | ||
68 | export const unusedActorAttributesForAPI = [ | 68 | export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = [ |
69 | 'publicKey', | 69 | 'publicKey', |
70 | 'privateKey', | 70 | 'privateKey', |
71 | 'inboxUrl', | 71 | 'inboxUrl', |
@@ -306,6 +306,27 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
306 | }) | 306 | }) |
307 | VideoChannel: VideoChannelModel | 307 | VideoChannel: VideoChannelModel |
308 | 308 | ||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
312 | return buildSQLAttributes({ | ||
313 | model: this, | ||
314 | tableName, | ||
315 | aliasPrefix | ||
316 | }) | ||
317 | } | ||
318 | |||
319 | static getSQLAPIAttributes (tableName: string, aliasPrefix = '') { | ||
320 | return buildSQLAttributes({ | ||
321 | model: this, | ||
322 | tableName, | ||
323 | aliasPrefix, | ||
324 | excludeAttributes: unusedActorAttributesForAPI | ||
325 | }) | ||
326 | } | ||
327 | |||
328 | // --------------------------------------------------------------------------- | ||
329 | |||
309 | static async load (id: number): Promise<MActor> { | 330 | static async load (id: number): Promise<MActor> { |
310 | const actorServer = await getServerActor() | 331 | const actorServer = await getServerActor() |
311 | if (id === actorServer.id) return actorServer | 332 | if (id === actorServer.id) return actorServer |
diff --git a/server/models/actor/sql/instance-list-followers-query-builder.ts b/server/models/actor/sql/instance-list-followers-query-builder.ts index 4a17a8f11..34ce29b5d 100644 --- a/server/models/actor/sql/instance-list-followers-query-builder.ts +++ b/server/models/actor/sql/instance-list-followers-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowersOptions { | 8 | export interface ListFollowersOptions { |
diff --git a/server/models/actor/sql/instance-list-following-query-builder.ts b/server/models/actor/sql/instance-list-following-query-builder.ts index 880170b85..77b4e3dce 100644 --- a/server/models/actor/sql/instance-list-following-query-builder.ts +++ b/server/models/actor/sql/instance-list-following-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowingOptions { | 8 | export interface ListFollowingOptions { |
diff --git a/server/models/actor/sql/shared/actor-follow-table-attributes.ts b/server/models/actor/sql/shared/actor-follow-table-attributes.ts index 156b37d44..7dd908ece 100644 --- a/server/models/actor/sql/shared/actor-follow-table-attributes.ts +++ b/server/models/actor/sql/shared/actor-follow-table-attributes.ts | |||
@@ -1,62 +1,31 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { Memoize } from '@server/helpers/memoize' | ||
3 | import { ServerModel } from '@server/models/server/server' | ||
4 | import { ActorModel } from '../../actor' | ||
5 | import { ActorFollowModel } from '../../actor-follow' | ||
6 | import { ActorImageModel } from '../../actor-image' | ||
7 | |||
1 | export class ActorFollowTableAttributes { | 8 | export class ActorFollowTableAttributes { |
2 | 9 | ||
10 | @Memoize() | ||
3 | getFollowAttributes () { | 11 | getFollowAttributes () { |
4 | return [ | 12 | logger.error('coucou') |
5 | '"ActorFollowModel"."id"', | 13 | |
6 | '"ActorFollowModel"."state"', | 14 | return ActorFollowModel.getSQLAttributes('ActorFollowModel').join(', ') |
7 | '"ActorFollowModel"."score"', | ||
8 | '"ActorFollowModel"."url"', | ||
9 | '"ActorFollowModel"."actorId"', | ||
10 | '"ActorFollowModel"."targetActorId"', | ||
11 | '"ActorFollowModel"."createdAt"', | ||
12 | '"ActorFollowModel"."updatedAt"' | ||
13 | ].join(', ') | ||
14 | } | 15 | } |
15 | 16 | ||
17 | @Memoize() | ||
16 | getActorAttributes (actorTableName: string) { | 18 | getActorAttributes (actorTableName: string) { |
17 | return [ | 19 | return ActorModel.getSQLAttributes(actorTableName, `${actorTableName}.`).join(', ') |
18 | `"${actorTableName}"."id" AS "${actorTableName}.id"`, | ||
19 | `"${actorTableName}"."type" AS "${actorTableName}.type"`, | ||
20 | `"${actorTableName}"."preferredUsername" AS "${actorTableName}.preferredUsername"`, | ||
21 | `"${actorTableName}"."url" AS "${actorTableName}.url"`, | ||
22 | `"${actorTableName}"."publicKey" AS "${actorTableName}.publicKey"`, | ||
23 | `"${actorTableName}"."privateKey" AS "${actorTableName}.privateKey"`, | ||
24 | `"${actorTableName}"."followersCount" AS "${actorTableName}.followersCount"`, | ||
25 | `"${actorTableName}"."followingCount" AS "${actorTableName}.followingCount"`, | ||
26 | `"${actorTableName}"."inboxUrl" AS "${actorTableName}.inboxUrl"`, | ||
27 | `"${actorTableName}"."outboxUrl" AS "${actorTableName}.outboxUrl"`, | ||
28 | `"${actorTableName}"."sharedInboxUrl" AS "${actorTableName}.sharedInboxUrl"`, | ||
29 | `"${actorTableName}"."followersUrl" AS "${actorTableName}.followersUrl"`, | ||
30 | `"${actorTableName}"."followingUrl" AS "${actorTableName}.followingUrl"`, | ||
31 | `"${actorTableName}"."remoteCreatedAt" AS "${actorTableName}.remoteCreatedAt"`, | ||
32 | `"${actorTableName}"."serverId" AS "${actorTableName}.serverId"`, | ||
33 | `"${actorTableName}"."createdAt" AS "${actorTableName}.createdAt"`, | ||
34 | `"${actorTableName}"."updatedAt" AS "${actorTableName}.updatedAt"` | ||
35 | ].join(', ') | ||
36 | } | 20 | } |
37 | 21 | ||
22 | @Memoize() | ||
38 | getServerAttributes (actorTableName: string) { | 23 | getServerAttributes (actorTableName: string) { |
39 | return [ | 24 | return ServerModel.getSQLAttributes(`${actorTableName}->Server`, `${actorTableName}.Server.`).join(', ') |
40 | `"${actorTableName}->Server"."id" AS "${actorTableName}.Server.id"`, | ||
41 | `"${actorTableName}->Server"."host" AS "${actorTableName}.Server.host"`, | ||
42 | `"${actorTableName}->Server"."redundancyAllowed" AS "${actorTableName}.Server.redundancyAllowed"`, | ||
43 | `"${actorTableName}->Server"."createdAt" AS "${actorTableName}.Server.createdAt"`, | ||
44 | `"${actorTableName}->Server"."updatedAt" AS "${actorTableName}.Server.updatedAt"` | ||
45 | ].join(', ') | ||
46 | } | 25 | } |
47 | 26 | ||
27 | @Memoize() | ||
48 | getAvatarAttributes (actorTableName: string) { | 28 | getAvatarAttributes (actorTableName: string) { |
49 | return [ | 29 | return ActorImageModel.getSQLAttributes(`${actorTableName}->Avatars`, `${actorTableName}.Avatars.`).join(', ') |
50 | `"${actorTableName}->Avatars"."id" AS "${actorTableName}.Avatars.id"`, | ||
51 | `"${actorTableName}->Avatars"."filename" AS "${actorTableName}.Avatars.filename"`, | ||
52 | `"${actorTableName}->Avatars"."height" AS "${actorTableName}.Avatars.height"`, | ||
53 | `"${actorTableName}->Avatars"."width" AS "${actorTableName}.Avatars.width"`, | ||
54 | `"${actorTableName}->Avatars"."fileUrl" AS "${actorTableName}.Avatars.fileUrl"`, | ||
55 | `"${actorTableName}->Avatars"."onDisk" AS "${actorTableName}.Avatars.onDisk"`, | ||
56 | `"${actorTableName}->Avatars"."type" AS "${actorTableName}.Avatars.type"`, | ||
57 | `"${actorTableName}->Avatars"."actorId" AS "${actorTableName}.Avatars.actorId"`, | ||
58 | `"${actorTableName}->Avatars"."createdAt" AS "${actorTableName}.Avatars.createdAt"`, | ||
59 | `"${actorTableName}->Avatars"."updatedAt" AS "${actorTableName}.Avatars.updatedAt"` | ||
60 | ].join(', ') | ||
61 | } | 30 | } |
62 | } | 31 | } |
diff --git a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts index 1d70fbe70..d9593e48b 100644 --- a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts +++ b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery } from '@server/models/shared' | 2 | import { AbstractRunQuery } from '@server/models/shared' |
3 | import { getInstanceFollowsSort } from '@server/models/utils' | ||
4 | import { ActorImageType } from '@shared/models' | 3 | import { ActorImageType } from '@shared/models' |
4 | import { getInstanceFollowsSort } from '../../../shared' | ||
5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' | 5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' |
6 | 6 | ||
7 | type BaseOptions = { | 7 | type BaseOptions = { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 15909d5f3..c2a72b71f 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -34,7 +34,7 @@ import { CONFIG } from '../../initializers/config' | |||
34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
35 | import { ActorModel } from '../actor/actor' | 35 | import { ActorModel } from '../actor/actor' |
36 | import { ServerModel } from '../server/server' | 36 | import { ServerModel } from '../server/server' |
37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared' |
38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | 38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' |
39 | import { VideoModel } from '../video/video' | 39 | import { VideoModel } from '../video/video' |
40 | import { VideoChannelModel } from '../video/video-channel' | 40 | import { VideoChannelModel } from '../video/video-channel' |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 71c205ffa..9948c9f7a 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | isPluginStableVersionValid, | 11 | isPluginStableVersionValid, |
12 | isPluginTypeValid | 12 | isPluginTypeValid |
13 | } from '../../helpers/custom-validators/plugins' | 13 | } from '../../helpers/custom-validators/plugins' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | 15 | ||
16 | @DefaultScope(() => ({ | 16 | @DefaultScope(() => ({ |
17 | attributes: { | 17 | attributes: { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9752dfbc3..3d755fe4a 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -4,7 +4,7 @@ import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormat | |||
4 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
9 | 9 | ||
10 | enum ScopeNames { | 10 | enum ScopeNames { |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ef42de090..a5e05f460 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -4,7 +4,7 @@ import { MServer, MServerFormattable } from '@server/types/models/server' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { isHostValid } from '../../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../../helpers/custom-validators/servers' |
6 | import { ActorModel } from '../actor/actor' | 6 | import { ActorModel } from '../actor/actor' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
8 | import { ServerBlocklistModel } from './server-blocklist' | 8 | import { ServerBlocklistModel } from './server-blocklist' |
9 | 9 | ||
10 | @Table({ | 10 | @Table({ |
@@ -52,6 +52,18 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> { | |||
52 | }) | 52 | }) |
53 | BlockedBy: ServerBlocklistModel[] | 53 | BlockedBy: ServerBlocklistModel[] |
54 | 54 | ||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
58 | return buildSQLAttributes({ | ||
59 | model: this, | ||
60 | tableName, | ||
61 | aliasPrefix | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
55 | static load (id: number, transaction?: Transaction): Promise<MServer> { | 67 | static load (id: number, transaction?: Transaction): Promise<MServer> { |
56 | const query = { | 68 | const query = { |
57 | where: { | 69 | where: { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 04528929c..5a7621e4d 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,4 +1,8 @@ | |||
1 | export * from './abstract-run-query' | 1 | export * from './abstract-run-query' |
2 | export * from './model-builder' | 2 | export * from './model-builder' |
3 | export * from './model-cache' | ||
3 | export * from './query' | 4 | export * from './query' |
5 | export * from './sequelize-helpers' | ||
6 | export * from './sort' | ||
7 | export * from './sql' | ||
4 | export * from './update' | 8 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts index c015ca4f5..07f7c4038 100644 --- a/server/models/shared/model-builder.ts +++ b/server/models/shared/model-builder.ts | |||
@@ -1,7 +1,24 @@ | |||
1 | import { isPlainObject } from 'lodash' | 1 | import { isPlainObject } from 'lodash' |
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | 2 | import { Model as SequelizeModel, ModelStatic, Sequelize } from 'sequelize' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | 4 | ||
5 | /** | ||
6 | * | ||
7 | * Build Sequelize models from sequelize raw query (that must use { nest: true } options) | ||
8 | * | ||
9 | * In order to sequelize to correctly build the JSON this class will ingest, | ||
10 | * the columns selected in the raw query should be in the following form: | ||
11 | * * All tables must be Pascal Cased (for example "VideoChannel") | ||
12 | * * Root table must end with `Model` (for example "VideoCommentModel") | ||
13 | * * Joined tables must contain the origin table name + '->JoinedTable'. For example: | ||
14 | * * "Actor" is joined to "Account": "Actor" table must be renamed "Account->Actor" | ||
15 | * * "Account->Actor" is joined to "Server": "Server" table must be renamed to "Account->Actor->Server" | ||
16 | * * Selected columns must be renamed to contain the JSON path: | ||
17 | * * "videoComment"."id": "VideoCommentModel"."id" | ||
18 | * * "Account"."Actor"."Server"."id": "Account.Actor.Server.id" | ||
19 | * * All tables must contain the row id | ||
20 | */ | ||
21 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | 22 | export class ModelBuilder <T extends SequelizeModel> { |
6 | private readonly modelRegistry = new Map<string, T>() | 23 | private readonly modelRegistry = new Map<string, T>() |
7 | 24 | ||
@@ -72,18 +89,18 @@ export class ModelBuilder <T extends SequelizeModel> { | |||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | 89 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), |
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | 90 | { existing: this.sequelize.modelManager.all.map(m => m.name) } |
74 | ) | 91 | ) |
75 | return undefined | 92 | return { created: false, model: null } |
76 | } | 93 | } |
77 | 94 | ||
78 | // FIXME: typings | 95 | const model = Model.build(json, { raw: true, isNewRecord: false }) |
79 | const model = new (Model as any)(json) | 96 | |
80 | this.modelRegistry.set(registryKey, model) | 97 | this.modelRegistry.set(registryKey, model) |
81 | 98 | ||
82 | return { created: true, model } | 99 | return { created: true, model } |
83 | } | 100 | } |
84 | 101 | ||
85 | private findModelBuilder (modelName: string) { | 102 | private findModelBuilder (modelName: string) { |
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | 103 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) as ModelStatic<T> |
87 | } | 104 | } |
88 | 105 | ||
89 | private buildSequelizeModelName (modelName: string) { | 106 | private buildSequelizeModelName (modelName: string) { |
diff --git a/server/models/model-cache.ts b/server/models/shared/model-cache.ts index 3651267e7..3651267e7 100644 --- a/server/models/model-cache.ts +++ b/server/models/shared/model-cache.ts | |||
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts index 036cc13c6..934acc21f 100644 --- a/server/models/shared/query.ts +++ b/server/models/shared/query.ts | |||
@@ -1,17 +1,82 @@ | |||
1 | import { BindOrReplacements, QueryTypes } from 'sequelize' | 1 | import { BindOrReplacements, Op, QueryTypes, Sequelize } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | 2 | import validator from 'validator' |
3 | import { forceNumber } from '@shared/core-utils' | ||
3 | 4 | ||
4 | function doesExist (query: string, bind?: BindOrReplacements) { | 5 | function doesExist (sequelize: Sequelize, query: string, bind?: BindOrReplacements) { |
5 | const options = { | 6 | const options = { |
6 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 7 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
7 | bind, | 8 | bind, |
8 | raw: true | 9 | raw: true |
9 | } | 10 | } |
10 | 11 | ||
11 | return sequelizeTypescript.query(query, options) | 12 | return sequelize.query(query, options) |
12 | .then(results => results.length === 1) | 13 | .then(results => results.length === 1) |
13 | } | 14 | } |
14 | 15 | ||
16 | function createSimilarityAttribute (col: string, value: string) { | ||
17 | return Sequelize.fn( | ||
18 | 'similarity', | ||
19 | |||
20 | searchTrigramNormalizeCol(col), | ||
21 | |||
22 | searchTrigramNormalizeValue(value) | ||
23 | ) | ||
24 | } | ||
25 | |||
26 | function buildWhereIdOrUUID (id: number | string) { | ||
27 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
28 | } | ||
29 | |||
30 | function parseAggregateResult (result: any) { | ||
31 | if (!result) return 0 | ||
32 | |||
33 | const total = forceNumber(result) | ||
34 | if (isNaN(total)) return 0 | ||
35 | |||
36 | return total | ||
37 | } | ||
38 | |||
39 | function parseRowCountResult (result: any) { | ||
40 | if (result.length !== 0) return result[0].total | ||
41 | |||
42 | return 0 | ||
43 | } | ||
44 | |||
45 | function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) { | ||
46 | return toEscape.map(t => { | ||
47 | return t === null | ||
48 | ? null | ||
49 | : sequelize.escape('' + t) | ||
50 | }).concat(additionalUnescaped).join(', ') | ||
51 | } | ||
52 | |||
53 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
54 | if (!sourceField) return {} | ||
55 | |||
56 | return { | ||
57 | [targetField]: { | ||
58 | // FIXME: ts error | ||
59 | [Op.iLike as any]: `%${sourceField}%` | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
15 | export { | 64 | export { |
16 | doesExist | 65 | doesExist, |
66 | createSimilarityAttribute, | ||
67 | buildWhereIdOrUUID, | ||
68 | parseAggregateResult, | ||
69 | parseRowCountResult, | ||
70 | createSafeIn, | ||
71 | searchAttribute | ||
72 | } | ||
73 | |||
74 | // --------------------------------------------------------------------------- | ||
75 | |||
76 | function searchTrigramNormalizeValue (value: string) { | ||
77 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
78 | } | ||
79 | |||
80 | function searchTrigramNormalizeCol (col: string) { | ||
81 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
17 | } | 82 | } |
diff --git a/server/models/shared/sequelize-helpers.ts b/server/models/shared/sequelize-helpers.ts new file mode 100644 index 000000000..7af8471dc --- /dev/null +++ b/server/models/shared/sequelize-helpers.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | |||
3 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
4 | if (!model.createdAt || !model.updatedAt) { | ||
5 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
6 | } | ||
7 | |||
8 | const now = Date.now() | ||
9 | const createdAtTime = model.createdAt.getTime() | ||
10 | const updatedAtTime = model.updatedAt.getTime() | ||
11 | |||
12 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
13 | } | ||
14 | |||
15 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
16 | if (nullable && (value === null || value === undefined)) return | ||
17 | |||
18 | if (validator(value) === false) { | ||
19 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
20 | } | ||
21 | } | ||
22 | |||
23 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
24 | return { | ||
25 | name: indexName, | ||
26 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
27 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
28 | using: 'gin', | ||
29 | operator: 'gin_trgm_ops' | ||
30 | } | ||
31 | } | ||
32 | |||
33 | // --------------------------------------------------------------------------- | ||
34 | |||
35 | export { | ||
36 | throwIfNotValid, | ||
37 | buildTrigramSearchIndex, | ||
38 | isOutdated | ||
39 | } | ||
diff --git a/server/models/shared/sort.ts b/server/models/shared/sort.ts new file mode 100644 index 000000000..d923072f2 --- /dev/null +++ b/server/models/shared/sort.ts | |||
@@ -0,0 +1,146 @@ | |||
1 | import { literal, OrderItem, Sequelize } from 'sequelize' | ||
2 | |||
3 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
4 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
5 | const { direction, field } = buildSortDirectionAndField(value) | ||
6 | |||
7 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
8 | |||
9 | if (field.toLowerCase() === 'match') { // Search | ||
10 | finalField = Sequelize.col('similarity') | ||
11 | } else { | ||
12 | finalField = field | ||
13 | } | ||
14 | |||
15 | return [ [ finalField, direction ], lastSort ] | ||
16 | } | ||
17 | |||
18 | function getAdminUsersSort (value: string): OrderItem[] { | ||
19 | const { direction, field } = buildSortDirectionAndField(value) | ||
20 | |||
21 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
22 | |||
23 | if (field === 'videoQuotaUsed') { // Users list | ||
24 | finalField = Sequelize.col('videoQuotaUsed') | ||
25 | } else { | ||
26 | finalField = field | ||
27 | } | ||
28 | |||
29 | const nullPolicy = direction === 'ASC' | ||
30 | ? 'NULLS FIRST' | ||
31 | : 'NULLS LAST' | ||
32 | |||
33 | // FIXME: typings | ||
34 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
35 | } | ||
36 | |||
37 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
38 | const { direction, field } = buildSortDirectionAndField(value) | ||
39 | |||
40 | if (field.toLowerCase() === 'name') { | ||
41 | return [ [ 'displayName', direction ], lastSort ] | ||
42 | } | ||
43 | |||
44 | return getSort(value, lastSort) | ||
45 | } | ||
46 | |||
47 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
48 | const { direction, field } = buildSortDirectionAndField(value) | ||
49 | |||
50 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
51 | return [ | ||
52 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
53 | |||
54 | [ Sequelize.col('VideoModel.views'), direction ], | ||
55 | |||
56 | lastSort | ||
57 | ] | ||
58 | } else if (field === 'publishedAt') { | ||
59 | return [ | ||
60 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
61 | |||
62 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
63 | |||
64 | lastSort | ||
65 | ] | ||
66 | } | ||
67 | |||
68 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
69 | |||
70 | // Alias | ||
71 | if (field.toLowerCase() === 'match') { // Search | ||
72 | finalField = Sequelize.col('similarity') | ||
73 | } else { | ||
74 | finalField = field | ||
75 | } | ||
76 | |||
77 | const firstSort: OrderItem = typeof finalField === 'string' | ||
78 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
79 | : [ finalField, direction ] | ||
80 | |||
81 | return [ firstSort, lastSort ] | ||
82 | } | ||
83 | |||
84 | function getBlacklistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
85 | const { direction, field } = buildSortDirectionAndField(value) | ||
86 | |||
87 | const videoFields = new Set([ 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid' ]) | ||
88 | |||
89 | if (videoFields.has(field)) { | ||
90 | return [ | ||
91 | [ literal(`"Video.${field}" ${direction}`) ], | ||
92 | lastSort | ||
93 | ] as OrderItem[] | ||
94 | } | ||
95 | |||
96 | return getSort(value, lastSort) | ||
97 | } | ||
98 | |||
99 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
100 | const { direction, field } = buildSortDirectionAndField(value) | ||
101 | |||
102 | if (field === 'redundancyAllowed') { | ||
103 | return [ | ||
104 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
105 | lastSort | ||
106 | ] | ||
107 | } | ||
108 | |||
109 | return getSort(value, lastSort) | ||
110 | } | ||
111 | |||
112 | function getChannelSyncSort (value: string): OrderItem[] { | ||
113 | const { direction, field } = buildSortDirectionAndField(value) | ||
114 | if (field.toLowerCase() === 'videochannel') { | ||
115 | return [ | ||
116 | [ literal('"VideoChannel.name"'), direction ] | ||
117 | ] | ||
118 | } | ||
119 | return [ [ field, direction ] ] | ||
120 | } | ||
121 | |||
122 | function buildSortDirectionAndField (value: string) { | ||
123 | let field: string | ||
124 | let direction: 'ASC' | 'DESC' | ||
125 | |||
126 | if (value.substring(0, 1) === '-') { | ||
127 | direction = 'DESC' | ||
128 | field = value.substring(1) | ||
129 | } else { | ||
130 | direction = 'ASC' | ||
131 | field = value | ||
132 | } | ||
133 | |||
134 | return { direction, field } | ||
135 | } | ||
136 | |||
137 | export { | ||
138 | buildSortDirectionAndField, | ||
139 | getPlaylistSort, | ||
140 | getSort, | ||
141 | getAdminUsersSort, | ||
142 | getVideoSort, | ||
143 | getBlacklistSort, | ||
144 | getChannelSyncSort, | ||
145 | getInstanceFollowsSort | ||
146 | } | ||
diff --git a/server/models/shared/sql.ts b/server/models/shared/sql.ts new file mode 100644 index 000000000..5aaeb49f0 --- /dev/null +++ b/server/models/shared/sql.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { literal, Model, ModelStatic } from 'sequelize' | ||
2 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | |||
5 | function buildLocalAccountIdsIn () { | ||
6 | return literal( | ||
7 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | function buildLocalActorIdsIn () { | ||
12 | return literal( | ||
13 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
14 | ) | ||
15 | } | ||
16 | |||
17 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
18 | const blockerIdsString = blockerIds.join(', ') | ||
19 | |||
20 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
21 | ' UNION ' + | ||
22 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
23 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
24 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
25 | } | ||
26 | |||
27 | function buildServerIdsFollowedBy (actorId: any) { | ||
28 | const actorIdNumber = forceNumber(actorId) | ||
29 | |||
30 | return '(' + | ||
31 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
32 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
33 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
34 | ')' | ||
35 | } | ||
36 | |||
37 | function buildSQLAttributes<M extends Model> (options: { | ||
38 | model: ModelStatic<M> | ||
39 | tableName: string | ||
40 | |||
41 | excludeAttributes?: Exclude<keyof AttributesOnly<M>, symbol>[] | ||
42 | aliasPrefix?: string | ||
43 | }) { | ||
44 | const { model, tableName, aliasPrefix, excludeAttributes } = options | ||
45 | |||
46 | const attributes = Object.keys(model.getAttributes()) as Exclude<keyof AttributesOnly<M>, symbol>[] | ||
47 | |||
48 | return attributes | ||
49 | .filter(a => { | ||
50 | if (!excludeAttributes) return true | ||
51 | if (excludeAttributes.includes(a)) return false | ||
52 | |||
53 | return true | ||
54 | }) | ||
55 | .map(a => { | ||
56 | return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"` | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | buildSQLAttributes, | ||
64 | buildBlockedAccountSQL, | ||
65 | buildServerIdsFollowedBy, | ||
66 | buildLocalAccountIdsIn, | ||
67 | buildLocalActorIdsIn | ||
68 | } | ||
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts index d338211e3..d02c4535d 100644 --- a/server/models/shared/update.ts +++ b/server/models/shared/update.ts | |||
@@ -1,9 +1,15 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | 1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | 2 | ||
4 | // Sequelize always skip the update if we only update updatedAt field | 3 | // Sequelize always skip the update if we only update updatedAt field |
5 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | 4 | function setAsUpdated (options: { |
6 | return sequelizeTypescript.query( | 5 | sequelize: Sequelize |
6 | table: string | ||
7 | id: number | ||
8 | transaction?: Transaction | ||
9 | }) { | ||
10 | const { sequelize, table, id, transaction } = options | ||
11 | |||
12 | return sequelize.query( | ||
7 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | 13 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, |
8 | { | 14 | { |
9 | replacements: { table, id, updatedAt: new Date() }, | 15 | replacements: { table, id, updatedAt: new Date() }, |
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts index 31b4932bf..7b29807a3 100644 --- a/server/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | 2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' |
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | 3 | import { UserNotificationModelForApi } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
5 | import { getSort } from '../../shared' | ||
6 | 6 | ||
7 | export interface ListNotificationsOptions { | 7 | export interface ListNotificationsOptions { |
8 | userId: number | 8 | userId: number |
@@ -180,7 +180,9 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
180 | "Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type", | 180 | "Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type", |
181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | 181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", |
182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | 182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", |
183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | 183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host", |
184 | "UserRegistration"."id" AS "UserRegistration.id", | ||
185 | "UserRegistration"."username" AS "UserRegistration.username"` | ||
184 | } | 186 | } |
185 | 187 | ||
186 | private getJoins () { | 188 | private getJoins () { |
@@ -196,74 +198,76 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { | |||
196 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" | 198 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" |
197 | ) ON "UserNotificationModel"."videoId" = "Video"."id" | 199 | ) ON "UserNotificationModel"."videoId" = "Video"."id" |
198 | 200 | ||
199 | LEFT JOIN ( | 201 | LEFT JOIN ( |
200 | "videoComment" AS "VideoComment" | 202 | "videoComment" AS "VideoComment" |
201 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" | 203 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" |
202 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" | 204 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" |
203 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" | 205 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" |
204 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" | 206 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" |
205 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | 207 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} |
206 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" | 208 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" |
207 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" | 209 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" |
208 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" | 210 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" |
209 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" | 211 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" |
212 | |||
213 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | ||
214 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | ||
215 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | ||
216 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | ||
217 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
218 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
219 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
220 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
221 | LEFT JOIN ( | ||
222 | "account" AS "Abuse->FlaggedAccount" | ||
223 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
224 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
225 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
226 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
227 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
228 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
229 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
210 | 230 | ||
211 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | 231 | LEFT JOIN ( |
212 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | 232 | "videoBlacklist" AS "VideoBlacklist" |
213 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | 233 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" |
214 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | 234 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" |
215 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
216 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
217 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
218 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
219 | LEFT JOIN ( | ||
220 | "account" AS "Abuse->FlaggedAccount" | ||
221 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
222 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
223 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
224 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
225 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
226 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
227 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
228 | 235 | ||
229 | LEFT JOIN ( | 236 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" |
230 | "videoBlacklist" AS "VideoBlacklist" | 237 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" |
231 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" | ||
232 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" | ||
233 | 238 | ||
234 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" | 239 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" |
235 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" | ||
236 | 240 | ||
237 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" | 241 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" |
238 | 242 | ||
239 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" | 243 | LEFT JOIN ( |
244 | "actorFollow" AS "ActorFollow" | ||
245 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | ||
246 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | ||
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | ||
248 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | ||
249 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | ||
250 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | ||
251 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
252 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
253 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
254 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
256 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
257 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
258 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
259 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
260 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
240 | 261 | ||
241 | LEFT JOIN ( | 262 | LEFT JOIN ( |
242 | "actorFollow" AS "ActorFollow" | 263 | "account" AS "Account" |
243 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | 264 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" |
244 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | 265 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" |
245 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | 266 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" |
246 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | 267 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} |
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | 268 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" |
248 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | 269 | ) ON "UserNotificationModel"."accountId" = "Account"."id" |
249 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
250 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
251 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
252 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
253 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
254 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
256 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
257 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
258 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
259 | 270 | ||
260 | LEFT JOIN ( | 271 | LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"` |
261 | "account" AS "Account" | ||
262 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" | ||
263 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" | ||
264 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" | ||
265 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
266 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" | ||
267 | ) ON "UserNotificationModel"."accountId" = "Account"."id"` | ||
268 | } | 272 | } |
269 | } | 273 | } |
diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 66e1d85b3..394494c0c 100644 --- a/server/models/user/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts | |||
@@ -17,7 +17,7 @@ import { MNotificationSettingFormattable } from '@server/types/models' | |||
17 | import { AttributesOnly } from '@shared/typescript-utils' | 17 | import { AttributesOnly } from '@shared/typescript-utils' |
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
20 | import { throwIfNotValid } from '../utils' | 20 | import { throwIfNotValid } from '../shared' |
21 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
22 | 22 | ||
23 | @Table({ | 23 | @Table({ |
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index d37fa5dc7..667ee7f5f 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -13,13 +13,14 @@ import { AccountModel } from '../account/account' | |||
13 | import { ActorFollowModel } from '../actor/actor-follow' | 13 | import { ActorFollowModel } from '../actor/actor-follow' |
14 | import { ApplicationModel } from '../application/application' | 14 | import { ApplicationModel } from '../application/application' |
15 | import { PluginModel } from '../server/plugin' | 15 | import { PluginModel } from '../server/plugin' |
16 | import { throwIfNotValid } from '../utils' | 16 | import { throwIfNotValid } from '../shared' |
17 | import { VideoModel } from '../video/video' | 17 | import { VideoModel } from '../video/video' |
18 | import { VideoBlacklistModel } from '../video/video-blacklist' | 18 | import { VideoBlacklistModel } from '../video/video-blacklist' |
19 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
20 | import { VideoImportModel } from '../video/video-import' | 20 | import { VideoImportModel } from '../video/video-import' |
21 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' | 21 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' |
22 | import { UserModel } from './user' | 22 | import { UserModel } from './user' |
23 | import { UserRegistrationModel } from './user-registration' | ||
23 | 24 | ||
24 | @Table({ | 25 | @Table({ |
25 | tableName: 'userNotification', | 26 | tableName: 'userNotification', |
@@ -98,6 +99,14 @@ import { UserModel } from './user' | |||
98 | [Op.ne]: null | 99 | [Op.ne]: null |
99 | } | 100 | } |
100 | } | 101 | } |
102 | }, | ||
103 | { | ||
104 | fields: [ 'userRegistrationId' ], | ||
105 | where: { | ||
106 | userRegistrationId: { | ||
107 | [Op.ne]: null | ||
108 | } | ||
109 | } | ||
101 | } | 110 | } |
102 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 111 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
103 | }) | 112 | }) |
@@ -241,6 +250,18 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
241 | }) | 250 | }) |
242 | Application: ApplicationModel | 251 | Application: ApplicationModel |
243 | 252 | ||
253 | @ForeignKey(() => UserRegistrationModel) | ||
254 | @Column | ||
255 | userRegistrationId: number | ||
256 | |||
257 | @BelongsTo(() => UserRegistrationModel, { | ||
258 | foreignKey: { | ||
259 | allowNull: true | ||
260 | }, | ||
261 | onDelete: 'cascade' | ||
262 | }) | ||
263 | UserRegistration: UserRegistrationModel | ||
264 | |||
244 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 265 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
245 | const where = { userId } | 266 | const where = { userId } |
246 | 267 | ||
@@ -416,6 +437,10 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
416 | ? { latestVersion: this.Application.latestPeerTubeVersion } | 437 | ? { latestVersion: this.Application.latestPeerTubeVersion } |
417 | : undefined | 438 | : undefined |
418 | 439 | ||
440 | const registration = this.UserRegistration | ||
441 | ? { id: this.UserRegistration.id, username: this.UserRegistration.username } | ||
442 | : undefined | ||
443 | |||
419 | return { | 444 | return { |
420 | id: this.id, | 445 | id: this.id, |
421 | type: this.type, | 446 | type: this.type, |
@@ -429,6 +454,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
429 | actorFollow, | 454 | actorFollow, |
430 | plugin, | 455 | plugin, |
431 | peertube, | 456 | peertube, |
457 | registration, | ||
432 | createdAt: this.createdAt.toISOString(), | 458 | createdAt: this.createdAt.toISOString(), |
433 | updatedAt: this.updatedAt.toISOString() | 459 | updatedAt: this.updatedAt.toISOString() |
434 | } | 460 | } |
diff --git a/server/models/user/user-registration.ts b/server/models/user/user-registration.ts new file mode 100644 index 000000000..adda3cc7e --- /dev/null +++ b/server/models/user/user-registration.ts | |||
@@ -0,0 +1,259 @@ | |||
1 | import { FindOptions, Op, WhereOptions } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BeforeCreate, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | IsEmail, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { | ||
17 | isRegistrationModerationResponseValid, | ||
18 | isRegistrationReasonValid, | ||
19 | isRegistrationStateValid | ||
20 | } from '@server/helpers/custom-validators/user-registration' | ||
21 | import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validators/video-channels' | ||
22 | import { cryptPassword } from '@server/helpers/peertube-crypto' | ||
23 | import { USER_REGISTRATION_STATES } from '@server/initializers/constants' | ||
24 | import { MRegistration, MRegistrationFormattable } from '@server/types/models' | ||
25 | import { UserRegistration, UserRegistrationState } from '@shared/models' | ||
26 | import { AttributesOnly } from '@shared/typescript-utils' | ||
27 | import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users' | ||
28 | import { getSort, throwIfNotValid } from '../shared' | ||
29 | import { UserModel } from './user' | ||
30 | |||
31 | @Table({ | ||
32 | tableName: 'userRegistration', | ||
33 | indexes: [ | ||
34 | { | ||
35 | fields: [ 'username' ], | ||
36 | unique: true | ||
37 | }, | ||
38 | { | ||
39 | fields: [ 'email' ], | ||
40 | unique: true | ||
41 | }, | ||
42 | { | ||
43 | fields: [ 'channelHandle' ], | ||
44 | unique: true | ||
45 | }, | ||
46 | { | ||
47 | fields: [ 'userId' ], | ||
48 | unique: true | ||
49 | } | ||
50 | ] | ||
51 | }) | ||
52 | export class UserRegistrationModel extends Model<Partial<AttributesOnly<UserRegistrationModel>>> { | ||
53 | |||
54 | @AllowNull(false) | ||
55 | @Is('RegistrationState', value => throwIfNotValid(value, isRegistrationStateValid, 'state')) | ||
56 | @Column | ||
57 | state: UserRegistrationState | ||
58 | |||
59 | @AllowNull(false) | ||
60 | @Is('RegistrationReason', value => throwIfNotValid(value, isRegistrationReasonValid, 'registration reason')) | ||
61 | @Column(DataType.TEXT) | ||
62 | registrationReason: string | ||
63 | |||
64 | @AllowNull(true) | ||
65 | @Is('RegistrationModerationResponse', value => throwIfNotValid(value, isRegistrationModerationResponseValid, 'moderation response', true)) | ||
66 | @Column(DataType.TEXT) | ||
67 | moderationResponse: string | ||
68 | |||
69 | @AllowNull(true) | ||
70 | @Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true)) | ||
71 | @Column | ||
72 | password: string | ||
73 | |||
74 | @AllowNull(false) | ||
75 | @Column | ||
76 | username: string | ||
77 | |||
78 | @AllowNull(false) | ||
79 | @IsEmail | ||
80 | @Column(DataType.STRING(400)) | ||
81 | email: string | ||
82 | |||
83 | @AllowNull(true) | ||
84 | @Is('RegistrationEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) | ||
85 | @Column | ||
86 | emailVerified: boolean | ||
87 | |||
88 | @AllowNull(true) | ||
89 | @Is('RegistrationAccountDisplayName', value => throwIfNotValid(value, isUserDisplayNameValid, 'account display name', true)) | ||
90 | @Column | ||
91 | accountDisplayName: string | ||
92 | |||
93 | @AllowNull(true) | ||
94 | @Is('ChannelHandle', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel handle', true)) | ||
95 | @Column | ||
96 | channelHandle: string | ||
97 | |||
98 | @AllowNull(true) | ||
99 | @Is('ChannelDisplayName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel display name', true)) | ||
100 | @Column | ||
101 | channelDisplayName: string | ||
102 | |||
103 | @CreatedAt | ||
104 | createdAt: Date | ||
105 | |||
106 | @UpdatedAt | ||
107 | updatedAt: Date | ||
108 | |||
109 | @ForeignKey(() => UserModel) | ||
110 | @Column | ||
111 | userId: number | ||
112 | |||
113 | @BelongsTo(() => UserModel, { | ||
114 | foreignKey: { | ||
115 | allowNull: true | ||
116 | }, | ||
117 | onDelete: 'SET NULL' | ||
118 | }) | ||
119 | User: UserModel | ||
120 | |||
121 | @BeforeCreate | ||
122 | static async cryptPasswordIfNeeded (instance: UserRegistrationModel) { | ||
123 | instance.password = await cryptPassword(instance.password) | ||
124 | } | ||
125 | |||
126 | static load (id: number): Promise<MRegistration> { | ||
127 | return UserRegistrationModel.findByPk(id) | ||
128 | } | ||
129 | |||
130 | static loadByEmail (email: string): Promise<MRegistration> { | ||
131 | const query = { | ||
132 | where: { email } | ||
133 | } | ||
134 | |||
135 | return UserRegistrationModel.findOne(query) | ||
136 | } | ||
137 | |||
138 | static loadByEmailOrUsername (emailOrUsername: string): Promise<MRegistration> { | ||
139 | const query = { | ||
140 | where: { | ||
141 | [Op.or]: [ | ||
142 | { email: emailOrUsername }, | ||
143 | { username: emailOrUsername } | ||
144 | ] | ||
145 | } | ||
146 | } | ||
147 | |||
148 | return UserRegistrationModel.findOne(query) | ||
149 | } | ||
150 | |||
151 | static loadByEmailOrHandle (options: { | ||
152 | email: string | ||
153 | username: string | ||
154 | channelHandle?: string | ||
155 | }): Promise<MRegistration> { | ||
156 | const { email, username, channelHandle } = options | ||
157 | |||
158 | let or: WhereOptions = [ | ||
159 | { email }, | ||
160 | { channelHandle: username }, | ||
161 | { username } | ||
162 | ] | ||
163 | |||
164 | if (channelHandle) { | ||
165 | or = or.concat([ | ||
166 | { username: channelHandle }, | ||
167 | { channelHandle } | ||
168 | ]) | ||
169 | } | ||
170 | |||
171 | const query = { | ||
172 | where: { | ||
173 | [Op.or]: or | ||
174 | } | ||
175 | } | ||
176 | |||
177 | return UserRegistrationModel.findOne(query) | ||
178 | } | ||
179 | |||
180 | // --------------------------------------------------------------------------- | ||
181 | |||
182 | static listForApi (options: { | ||
183 | start: number | ||
184 | count: number | ||
185 | sort: string | ||
186 | search?: string | ||
187 | }) { | ||
188 | const { start, count, sort, search } = options | ||
189 | |||
190 | const where: WhereOptions = {} | ||
191 | |||
192 | if (search) { | ||
193 | Object.assign(where, { | ||
194 | [Op.or]: [ | ||
195 | { | ||
196 | email: { | ||
197 | [Op.iLike]: '%' + search + '%' | ||
198 | } | ||
199 | }, | ||
200 | { | ||
201 | username: { | ||
202 | [Op.iLike]: '%' + search + '%' | ||
203 | } | ||
204 | } | ||
205 | ] | ||
206 | }) | ||
207 | } | ||
208 | |||
209 | const query: FindOptions = { | ||
210 | offset: start, | ||
211 | limit: count, | ||
212 | order: getSort(sort), | ||
213 | where, | ||
214 | include: [ | ||
215 | { | ||
216 | model: UserModel.unscoped(), | ||
217 | required: false | ||
218 | } | ||
219 | ] | ||
220 | } | ||
221 | |||
222 | return Promise.all([ | ||
223 | UserRegistrationModel.count(query), | ||
224 | UserRegistrationModel.findAll<MRegistrationFormattable>(query) | ||
225 | ]).then(([ total, data ]) => ({ total, data })) | ||
226 | } | ||
227 | |||
228 | // --------------------------------------------------------------------------- | ||
229 | |||
230 | toFormattedJSON (this: MRegistrationFormattable): UserRegistration { | ||
231 | return { | ||
232 | id: this.id, | ||
233 | |||
234 | state: { | ||
235 | id: this.state, | ||
236 | label: USER_REGISTRATION_STATES[this.state] | ||
237 | }, | ||
238 | |||
239 | registrationReason: this.registrationReason, | ||
240 | moderationResponse: this.moderationResponse, | ||
241 | |||
242 | username: this.username, | ||
243 | email: this.email, | ||
244 | emailVerified: this.emailVerified, | ||
245 | |||
246 | accountDisplayName: this.accountDisplayName, | ||
247 | |||
248 | channelHandle: this.channelHandle, | ||
249 | channelDisplayName: this.channelDisplayName, | ||
250 | |||
251 | createdAt: this.createdAt, | ||
252 | updatedAt: this.updatedAt, | ||
253 | |||
254 | user: this.User | ||
255 | ? { id: this.User.id } | ||
256 | : null | ||
257 | } | ||
258 | } | ||
259 | } | ||
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 3fd808edc..bfc9b3049 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -30,6 +30,7 @@ import { | |||
30 | MUserNotifSettingChannelDefault, | 30 | MUserNotifSettingChannelDefault, |
31 | MUserWithNotificationSetting | 31 | MUserWithNotificationSetting |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { forceNumber } from '@shared/core-utils' | ||
33 | import { AttributesOnly } from '@shared/typescript-utils' | 34 | import { AttributesOnly } from '@shared/typescript-utils' |
34 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
35 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' |
@@ -63,14 +64,13 @@ import { ActorModel } from '../actor/actor' | |||
63 | import { ActorFollowModel } from '../actor/actor-follow' | 64 | import { ActorFollowModel } from '../actor/actor-follow' |
64 | import { ActorImageModel } from '../actor/actor-image' | 65 | import { ActorImageModel } from '../actor/actor-image' |
65 | import { OAuthTokenModel } from '../oauth/oauth-token' | 66 | import { OAuthTokenModel } from '../oauth/oauth-token' |
66 | import { getAdminUsersSort, throwIfNotValid } from '../utils' | 67 | import { getAdminUsersSort, throwIfNotValid } from '../shared' |
67 | import { VideoModel } from '../video/video' | 68 | import { VideoModel } from '../video/video' |
68 | import { VideoChannelModel } from '../video/video-channel' | 69 | import { VideoChannelModel } from '../video/video-channel' |
69 | import { VideoImportModel } from '../video/video-import' | 70 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | 71 | import { VideoLiveModel } from '../video/video-live' |
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 72 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
73 | import { forceNumber } from '@shared/core-utils' | ||
74 | 74 | ||
75 | enum ScopeNames { | 75 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 76 | FOR_ME_API = 'FOR_ME_API', |
@@ -441,16 +441,17 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
441 | }) | 441 | }) |
442 | OAuthTokens: OAuthTokenModel[] | 442 | OAuthTokens: OAuthTokenModel[] |
443 | 443 | ||
444 | // Used if we already set an encrypted password in user model | ||
445 | skipPasswordEncryption = false | ||
446 | |||
444 | @BeforeCreate | 447 | @BeforeCreate |
445 | @BeforeUpdate | 448 | @BeforeUpdate |
446 | static cryptPasswordIfNeeded (instance: UserModel) { | 449 | static async cryptPasswordIfNeeded (instance: UserModel) { |
447 | if (instance.changed('password') && instance.password) { | 450 | if (instance.skipPasswordEncryption) return |
448 | return cryptPassword(instance.password) | 451 | if (!instance.changed('password')) return |
449 | .then(hash => { | 452 | if (!instance.password) return |
450 | instance.password = hash | 453 | |
451 | return undefined | 454 | instance.password = await cryptPassword(instance.password) |
452 | }) | ||
453 | } | ||
454 | } | 455 | } |
455 | 456 | ||
456 | @AfterUpdate | 457 | @AfterUpdate |
diff --git a/server/models/utils.ts b/server/models/utils.ts deleted file mode 100644 index 3476799ce..000000000 --- a/server/models/utils.ts +++ /dev/null | |||
@@ -1,317 +0,0 @@ | |||
1 | import { literal, Op, OrderItem, Sequelize } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | |||
5 | type SortType = { sortModel: string, sortValue: string } | ||
6 | |||
7 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
8 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
9 | const { direction, field } = buildDirectionAndField(value) | ||
10 | |||
11 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
12 | |||
13 | if (field.toLowerCase() === 'match') { // Search | ||
14 | finalField = Sequelize.col('similarity') | ||
15 | } else { | ||
16 | finalField = field | ||
17 | } | ||
18 | |||
19 | return [ [ finalField, direction ], lastSort ] | ||
20 | } | ||
21 | |||
22 | function getAdminUsersSort (value: string): OrderItem[] { | ||
23 | const { direction, field } = buildDirectionAndField(value) | ||
24 | |||
25 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
26 | |||
27 | if (field === 'videoQuotaUsed') { // Users list | ||
28 | finalField = Sequelize.col('videoQuotaUsed') | ||
29 | } else { | ||
30 | finalField = field | ||
31 | } | ||
32 | |||
33 | const nullPolicy = direction === 'ASC' | ||
34 | ? 'NULLS FIRST' | ||
35 | : 'NULLS LAST' | ||
36 | |||
37 | // FIXME: typings | ||
38 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
39 | } | ||
40 | |||
41 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
42 | const { direction, field } = buildDirectionAndField(value) | ||
43 | |||
44 | if (field.toLowerCase() === 'name') { | ||
45 | return [ [ 'displayName', direction ], lastSort ] | ||
46 | } | ||
47 | |||
48 | return getSort(value, lastSort) | ||
49 | } | ||
50 | |||
51 | function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
52 | const { direction, field } = buildDirectionAndField(value) | ||
53 | |||
54 | if (field === 'totalReplies') { | ||
55 | return [ | ||
56 | [ Sequelize.literal('"totalReplies"'), direction ], | ||
57 | lastSort | ||
58 | ] | ||
59 | } | ||
60 | |||
61 | return getSort(value, lastSort) | ||
62 | } | ||
63 | |||
64 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
65 | const { direction, field } = buildDirectionAndField(value) | ||
66 | |||
67 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
68 | return [ | ||
69 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
70 | |||
71 | [ Sequelize.col('VideoModel.views'), direction ], | ||
72 | |||
73 | lastSort | ||
74 | ] | ||
75 | } else if (field === 'publishedAt') { | ||
76 | return [ | ||
77 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
78 | |||
79 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
80 | |||
81 | lastSort | ||
82 | ] | ||
83 | } | ||
84 | |||
85 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
86 | |||
87 | // Alias | ||
88 | if (field.toLowerCase() === 'match') { // Search | ||
89 | finalField = Sequelize.col('similarity') | ||
90 | } else { | ||
91 | finalField = field | ||
92 | } | ||
93 | |||
94 | const firstSort: OrderItem = typeof finalField === 'string' | ||
95 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
96 | : [ finalField, direction ] | ||
97 | |||
98 | return [ firstSort, lastSort ] | ||
99 | } | ||
100 | |||
101 | function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
102 | const [ firstSort ] = getSort(value) | ||
103 | |||
104 | if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[] | ||
105 | return [ firstSort, lastSort ] | ||
106 | } | ||
107 | |||
108 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
109 | const { direction, field } = buildDirectionAndField(value) | ||
110 | |||
111 | if (field === 'redundancyAllowed') { | ||
112 | return [ | ||
113 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
114 | lastSort | ||
115 | ] | ||
116 | } | ||
117 | |||
118 | return getSort(value, lastSort) | ||
119 | } | ||
120 | |||
121 | function getChannelSyncSort (value: string): OrderItem[] { | ||
122 | const { direction, field } = buildDirectionAndField(value) | ||
123 | if (field.toLowerCase() === 'videochannel') { | ||
124 | return [ | ||
125 | [ literal('"VideoChannel.name"'), direction ] | ||
126 | ] | ||
127 | } | ||
128 | return [ [ field, direction ] ] | ||
129 | } | ||
130 | |||
131 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
132 | if (!model.createdAt || !model.updatedAt) { | ||
133 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
134 | } | ||
135 | |||
136 | const now = Date.now() | ||
137 | const createdAtTime = model.createdAt.getTime() | ||
138 | const updatedAtTime = model.updatedAt.getTime() | ||
139 | |||
140 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
141 | } | ||
142 | |||
143 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
144 | if (nullable && (value === null || value === undefined)) return | ||
145 | |||
146 | if (validator(value) === false) { | ||
147 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
148 | } | ||
149 | } | ||
150 | |||
151 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
152 | return { | ||
153 | name: indexName, | ||
154 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
155 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
156 | using: 'gin', | ||
157 | operator: 'gin_trgm_ops' | ||
158 | } | ||
159 | } | ||
160 | |||
161 | function createSimilarityAttribute (col: string, value: string) { | ||
162 | return Sequelize.fn( | ||
163 | 'similarity', | ||
164 | |||
165 | searchTrigramNormalizeCol(col), | ||
166 | |||
167 | searchTrigramNormalizeValue(value) | ||
168 | ) | ||
169 | } | ||
170 | |||
171 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
172 | const blockerIdsString = blockerIds.join(', ') | ||
173 | |||
174 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
175 | ' UNION ' + | ||
176 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
177 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
178 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
179 | } | ||
180 | |||
181 | function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) { | ||
182 | const blockerIdsString = blockerIds.join(', ') | ||
183 | |||
184 | return [ | ||
185 | literal( | ||
186 | `NOT EXISTS (` + | ||
187 | ` SELECT 1 FROM "accountBlocklist" ` + | ||
188 | ` WHERE "targetAccountId" = ${columnNameJoin} ` + | ||
189 | ` AND "accountId" IN (${blockerIdsString})` + | ||
190 | `)` | ||
191 | ), | ||
192 | |||
193 | literal( | ||
194 | `NOT EXISTS (` + | ||
195 | ` SELECT 1 FROM "account" ` + | ||
196 | ` INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
197 | ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
198 | ` WHERE "account"."id" = ${columnNameJoin} ` + | ||
199 | ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
200 | `)` | ||
201 | ) | ||
202 | ] | ||
203 | } | ||
204 | |||
205 | function buildServerIdsFollowedBy (actorId: any) { | ||
206 | const actorIdNumber = forceNumber(actorId) | ||
207 | |||
208 | return '(' + | ||
209 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
210 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
211 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
212 | ')' | ||
213 | } | ||
214 | |||
215 | function buildWhereIdOrUUID (id: number | string) { | ||
216 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
217 | } | ||
218 | |||
219 | function parseAggregateResult (result: any) { | ||
220 | if (!result) return 0 | ||
221 | |||
222 | const total = forceNumber(result) | ||
223 | if (isNaN(total)) return 0 | ||
224 | |||
225 | return total | ||
226 | } | ||
227 | |||
228 | function parseRowCountResult (result: any) { | ||
229 | if (result.length !== 0) return result[0].total | ||
230 | |||
231 | return 0 | ||
232 | } | ||
233 | |||
234 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { | ||
235 | return stringArr.map(t => { | ||
236 | return t === null | ||
237 | ? null | ||
238 | : sequelize.escape('' + t) | ||
239 | }).join(', ') | ||
240 | } | ||
241 | |||
242 | function buildLocalAccountIdsIn () { | ||
243 | return literal( | ||
244 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
245 | ) | ||
246 | } | ||
247 | |||
248 | function buildLocalActorIdsIn () { | ||
249 | return literal( | ||
250 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
251 | ) | ||
252 | } | ||
253 | |||
254 | function buildDirectionAndField (value: string) { | ||
255 | let field: string | ||
256 | let direction: 'ASC' | 'DESC' | ||
257 | |||
258 | if (value.substring(0, 1) === '-') { | ||
259 | direction = 'DESC' | ||
260 | field = value.substring(1) | ||
261 | } else { | ||
262 | direction = 'ASC' | ||
263 | field = value | ||
264 | } | ||
265 | |||
266 | return { direction, field } | ||
267 | } | ||
268 | |||
269 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
270 | if (!sourceField) return {} | ||
271 | |||
272 | return { | ||
273 | [targetField]: { | ||
274 | // FIXME: ts error | ||
275 | [Op.iLike as any]: `%${sourceField}%` | ||
276 | } | ||
277 | } | ||
278 | } | ||
279 | |||
280 | // --------------------------------------------------------------------------- | ||
281 | |||
282 | export { | ||
283 | buildBlockedAccountSQL, | ||
284 | buildBlockedAccountSQLOptimized, | ||
285 | buildLocalActorIdsIn, | ||
286 | getPlaylistSort, | ||
287 | SortType, | ||
288 | buildLocalAccountIdsIn, | ||
289 | getSort, | ||
290 | getCommentSort, | ||
291 | getAdminUsersSort, | ||
292 | getVideoSort, | ||
293 | getBlacklistSort, | ||
294 | getChannelSyncSort, | ||
295 | createSimilarityAttribute, | ||
296 | throwIfNotValid, | ||
297 | buildServerIdsFollowedBy, | ||
298 | buildTrigramSearchIndex, | ||
299 | buildWhereIdOrUUID, | ||
300 | isOutdated, | ||
301 | parseAggregateResult, | ||
302 | getInstanceFollowsSort, | ||
303 | buildDirectionAndField, | ||
304 | createSafeIn, | ||
305 | searchAttribute, | ||
306 | parseRowCountResult | ||
307 | } | ||
308 | |||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | function searchTrigramNormalizeValue (value: string) { | ||
312 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
313 | } | ||
314 | |||
315 | function searchTrigramNormalizeCol (col: string) { | ||
316 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
317 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index f285db477..6f05dbdc8 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -488,7 +488,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
488 | } | 488 | } |
489 | 489 | ||
490 | function getCategoryLabel (id: number) { | 490 | function getCategoryLabel (id: number) { |
491 | return VIDEO_CATEGORIES[id] || 'Misc' | 491 | return VIDEO_CATEGORIES[id] || 'Unknown' |
492 | } | 492 | } |
493 | 493 | ||
494 | function getLicenceLabel (id: number) { | 494 | function getLicenceLabel (id: number) { |
diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts new file mode 100644 index 000000000..a7eed22a1 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts | |||
@@ -0,0 +1,400 @@ | |||
1 | import { Model, Sequelize, Transaction } from 'sequelize' | ||
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | ||
3 | import { ActorImageType, VideoPrivacy } from '@shared/models' | ||
4 | import { createSafeIn, getSort, parseRowCountResult } from '../../../shared' | ||
5 | import { VideoCommentTableAttributes } from './video-comment-table-attributes' | ||
6 | |||
7 | export interface ListVideoCommentsOptions { | ||
8 | selectType: 'api' | 'feed' | 'comment-only' | ||
9 | |||
10 | start?: number | ||
11 | count?: number | ||
12 | sort?: string | ||
13 | |||
14 | videoId?: number | ||
15 | threadId?: number | ||
16 | accountId?: number | ||
17 | videoChannelId?: number | ||
18 | |||
19 | blockerAccountIds?: number[] | ||
20 | |||
21 | isThread?: boolean | ||
22 | notDeleted?: boolean | ||
23 | isLocal?: boolean | ||
24 | onLocalVideo?: boolean | ||
25 | onPublicVideo?: boolean | ||
26 | videoAccountOwnerId?: boolean | ||
27 | |||
28 | search?: string | ||
29 | searchAccount?: string | ||
30 | searchVideo?: string | ||
31 | |||
32 | includeReplyCounters?: boolean | ||
33 | |||
34 | transaction?: Transaction | ||
35 | } | ||
36 | |||
37 | export class VideoCommentListQueryBuilder extends AbstractRunQuery { | ||
38 | private readonly tableAttributes = new VideoCommentTableAttributes() | ||
39 | |||
40 | private innerQuery: string | ||
41 | |||
42 | private select = '' | ||
43 | private joins = '' | ||
44 | |||
45 | private innerSelect = '' | ||
46 | private innerJoins = '' | ||
47 | private innerLateralJoins = '' | ||
48 | private innerWhere = '' | ||
49 | |||
50 | private readonly built = { | ||
51 | cte: false, | ||
52 | accountJoin: false, | ||
53 | videoJoin: false, | ||
54 | videoChannelJoin: false, | ||
55 | avatarJoin: false | ||
56 | } | ||
57 | |||
58 | constructor ( | ||
59 | protected readonly sequelize: Sequelize, | ||
60 | private readonly options: ListVideoCommentsOptions | ||
61 | ) { | ||
62 | super(sequelize) | ||
63 | |||
64 | if (this.options.includeReplyCounters && !this.options.videoId) { | ||
65 | throw new Error('Cannot include reply counters without videoId') | ||
66 | } | ||
67 | } | ||
68 | |||
69 | async listComments <T extends Model> () { | ||
70 | this.buildListQuery() | ||
71 | |||
72 | const results = await this.runQuery({ nest: true, transaction: this.options.transaction }) | ||
73 | const modelBuilder = new ModelBuilder<T>(this.sequelize) | ||
74 | |||
75 | return modelBuilder.createModels(results, 'VideoComment') | ||
76 | } | ||
77 | |||
78 | async countComments () { | ||
79 | this.buildCountQuery() | ||
80 | |||
81 | const result = await this.runQuery({ transaction: this.options.transaction }) | ||
82 | |||
83 | return parseRowCountResult(result) | ||
84 | } | ||
85 | |||
86 | // --------------------------------------------------------------------------- | ||
87 | |||
88 | private buildListQuery () { | ||
89 | this.buildInnerListQuery() | ||
90 | this.buildListSelect() | ||
91 | |||
92 | this.query = `${this.select} ` + | ||
93 | `FROM (${this.innerQuery}) AS "VideoCommentModel" ` + | ||
94 | `${this.joins} ` + | ||
95 | `${this.getOrder()}` | ||
96 | } | ||
97 | |||
98 | private buildInnerListQuery () { | ||
99 | this.buildWhere() | ||
100 | this.buildInnerListSelect() | ||
101 | |||
102 | this.innerQuery = `${this.innerSelect} ` + | ||
103 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
104 | `${this.innerJoins} ` + | ||
105 | `${this.innerLateralJoins} ` + | ||
106 | `${this.innerWhere} ` + | ||
107 | `${this.getOrder()} ` + | ||
108 | `${this.getInnerLimit()}` | ||
109 | } | ||
110 | |||
111 | // --------------------------------------------------------------------------- | ||
112 | |||
113 | private buildCountQuery () { | ||
114 | this.buildWhere() | ||
115 | |||
116 | this.query = `SELECT COUNT(*) AS "total" ` + | ||
117 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
118 | `${this.innerJoins} ` + | ||
119 | `${this.innerWhere}` | ||
120 | } | ||
121 | |||
122 | // --------------------------------------------------------------------------- | ||
123 | |||
124 | private buildWhere () { | ||
125 | let where: string[] = [] | ||
126 | |||
127 | if (this.options.videoId) { | ||
128 | this.replacements.videoId = this.options.videoId | ||
129 | |||
130 | where.push('"VideoCommentModel"."videoId" = :videoId') | ||
131 | } | ||
132 | |||
133 | if (this.options.threadId) { | ||
134 | this.replacements.threadId = this.options.threadId | ||
135 | |||
136 | where.push('("VideoCommentModel"."id" = :threadId OR "VideoCommentModel"."originCommentId" = :threadId)') | ||
137 | } | ||
138 | |||
139 | if (this.options.accountId) { | ||
140 | this.replacements.accountId = this.options.accountId | ||
141 | |||
142 | where.push('"VideoCommentModel"."accountId" = :accountId') | ||
143 | } | ||
144 | |||
145 | if (this.options.videoChannelId) { | ||
146 | this.buildVideoChannelJoin() | ||
147 | |||
148 | this.replacements.videoChannelId = this.options.videoChannelId | ||
149 | |||
150 | where.push('"Account->VideoChannel"."id" = :videoChannelId') | ||
151 | } | ||
152 | |||
153 | if (this.options.blockerAccountIds) { | ||
154 | this.buildVideoChannelJoin() | ||
155 | |||
156 | where = where.concat(this.getBlockWhere('VideoCommentModel', 'Video->VideoChannel')) | ||
157 | } | ||
158 | |||
159 | if (this.options.isThread === true) { | ||
160 | where.push('"VideoCommentModel"."inReplyToCommentId" IS NULL') | ||
161 | } | ||
162 | |||
163 | if (this.options.notDeleted === true) { | ||
164 | where.push('"VideoCommentModel"."deletedAt" IS NULL') | ||
165 | } | ||
166 | |||
167 | if (this.options.isLocal === true) { | ||
168 | this.buildAccountJoin() | ||
169 | |||
170 | where.push('"Account->Actor"."serverId" IS NULL') | ||
171 | } else if (this.options.isLocal === false) { | ||
172 | this.buildAccountJoin() | ||
173 | |||
174 | where.push('"Account->Actor"."serverId" IS NOT NULL') | ||
175 | } | ||
176 | |||
177 | if (this.options.onLocalVideo === true) { | ||
178 | this.buildVideoJoin() | ||
179 | |||
180 | where.push('"Video"."remote" IS FALSE') | ||
181 | } else if (this.options.onLocalVideo === false) { | ||
182 | this.buildVideoJoin() | ||
183 | |||
184 | where.push('"Video"."remote" IS TRUE') | ||
185 | } | ||
186 | |||
187 | if (this.options.onPublicVideo === true) { | ||
188 | this.buildVideoJoin() | ||
189 | |||
190 | where.push(`"Video"."privacy" = ${VideoPrivacy.PUBLIC}`) | ||
191 | } | ||
192 | |||
193 | if (this.options.videoAccountOwnerId) { | ||
194 | this.buildVideoChannelJoin() | ||
195 | |||
196 | this.replacements.videoAccountOwnerId = this.options.videoAccountOwnerId | ||
197 | |||
198 | where.push(`"Video->VideoChannel"."accountId" = :videoAccountOwnerId`) | ||
199 | } | ||
200 | |||
201 | if (this.options.search) { | ||
202 | this.buildVideoJoin() | ||
203 | this.buildAccountJoin() | ||
204 | |||
205 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%') | ||
206 | |||
207 | where.push( | ||
208 | `(` + | ||
209 | `"VideoCommentModel"."text" ILIKE ${escapedLikeSearch} OR ` + | ||
210 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
211 | `"Account"."name" ILIKE ${escapedLikeSearch} OR ` + | ||
212 | `"Video"."name" ILIKE ${escapedLikeSearch} ` + | ||
213 | `)` | ||
214 | ) | ||
215 | } | ||
216 | |||
217 | if (this.options.searchAccount) { | ||
218 | this.buildAccountJoin() | ||
219 | |||
220 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchAccount + '%') | ||
221 | |||
222 | where.push( | ||
223 | `(` + | ||
224 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
225 | `"Account"."name" ILIKE ${escapedLikeSearch} ` + | ||
226 | `)` | ||
227 | ) | ||
228 | } | ||
229 | |||
230 | if (this.options.searchVideo) { | ||
231 | this.buildVideoJoin() | ||
232 | |||
233 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchVideo + '%') | ||
234 | |||
235 | where.push(`"Video"."name" ILIKE ${escapedLikeSearch}`) | ||
236 | } | ||
237 | |||
238 | if (where.length !== 0) { | ||
239 | this.innerWhere = `WHERE ${where.join(' AND ')}` | ||
240 | } | ||
241 | } | ||
242 | |||
243 | private buildAccountJoin () { | ||
244 | if (this.built.accountJoin) return | ||
245 | |||
246 | this.innerJoins += ' LEFT JOIN "account" "Account" ON "Account"."id" = "VideoCommentModel"."accountId" ' + | ||
247 | 'LEFT JOIN "actor" "Account->Actor" ON "Account->Actor"."id" = "Account"."actorId" ' + | ||
248 | 'LEFT JOIN "server" "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" ' | ||
249 | |||
250 | this.built.accountJoin = true | ||
251 | } | ||
252 | |||
253 | private buildVideoJoin () { | ||
254 | if (this.built.videoJoin) return | ||
255 | |||
256 | this.innerJoins += ' LEFT JOIN "video" "Video" ON "Video"."id" = "VideoCommentModel"."videoId" ' | ||
257 | |||
258 | this.built.videoJoin = true | ||
259 | } | ||
260 | |||
261 | private buildVideoChannelJoin () { | ||
262 | if (this.built.videoChannelJoin) return | ||
263 | |||
264 | this.buildVideoJoin() | ||
265 | |||
266 | this.innerJoins += ' LEFT JOIN "videoChannel" "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" ' | ||
267 | |||
268 | this.built.videoChannelJoin = true | ||
269 | } | ||
270 | |||
271 | private buildAvatarsJoin () { | ||
272 | if (this.options.selectType !== 'api' && this.options.selectType !== 'feed') return '' | ||
273 | if (this.built.avatarJoin) return | ||
274 | |||
275 | this.joins += `LEFT JOIN "actorImage" "Account->Actor->Avatars" ` + | ||
276 | `ON "VideoCommentModel"."Account.Actor.id" = "Account->Actor->Avatars"."actorId" ` + | ||
277 | `AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
278 | |||
279 | this.built.avatarJoin = true | ||
280 | } | ||
281 | |||
282 | // --------------------------------------------------------------------------- | ||
283 | |||
284 | private buildListSelect () { | ||
285 | const toSelect = [ '"VideoCommentModel".*' ] | ||
286 | |||
287 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
288 | this.buildAvatarsJoin() | ||
289 | |||
290 | toSelect.push(this.tableAttributes.getAvatarAttributes()) | ||
291 | } | ||
292 | |||
293 | this.select = this.buildSelect(toSelect) | ||
294 | } | ||
295 | |||
296 | private buildInnerListSelect () { | ||
297 | let toSelect = [ this.tableAttributes.getVideoCommentAttributes() ] | ||
298 | |||
299 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
300 | this.buildAccountJoin() | ||
301 | this.buildVideoJoin() | ||
302 | |||
303 | toSelect = toSelect.concat([ | ||
304 | this.tableAttributes.getVideoAttributes(), | ||
305 | this.tableAttributes.getAccountAttributes(), | ||
306 | this.tableAttributes.getActorAttributes(), | ||
307 | this.tableAttributes.getServerAttributes() | ||
308 | ]) | ||
309 | } | ||
310 | |||
311 | if (this.options.includeReplyCounters === true) { | ||
312 | this.buildTotalRepliesSelect() | ||
313 | this.buildAuthorTotalRepliesSelect() | ||
314 | |||
315 | toSelect.push('"totalRepliesFromVideoAuthor"."count" AS "totalRepliesFromVideoAuthor"') | ||
316 | toSelect.push('"totalReplies"."count" AS "totalReplies"') | ||
317 | } | ||
318 | |||
319 | this.innerSelect = this.buildSelect(toSelect) | ||
320 | } | ||
321 | |||
322 | // --------------------------------------------------------------------------- | ||
323 | |||
324 | private getBlockWhere (commentTableName: string, channelTableName: string) { | ||
325 | const where: string[] = [] | ||
326 | |||
327 | const blockerIdsString = createSafeIn( | ||
328 | this.sequelize, | ||
329 | this.options.blockerAccountIds, | ||
330 | [ `"${channelTableName}"."accountId"` ] | ||
331 | ) | ||
332 | |||
333 | where.push( | ||
334 | `NOT EXISTS (` + | ||
335 | `SELECT 1 FROM "accountBlocklist" ` + | ||
336 | `WHERE "targetAccountId" = "${commentTableName}"."accountId" ` + | ||
337 | `AND "accountId" IN (${blockerIdsString})` + | ||
338 | `)` | ||
339 | ) | ||
340 | |||
341 | where.push( | ||
342 | `NOT EXISTS (` + | ||
343 | `SELECT 1 FROM "account" ` + | ||
344 | `INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
345 | `INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
346 | `WHERE "account"."id" = "${commentTableName}"."accountId" ` + | ||
347 | `AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
348 | `)` | ||
349 | ) | ||
350 | |||
351 | return where | ||
352 | } | ||
353 | |||
354 | // --------------------------------------------------------------------------- | ||
355 | |||
356 | private buildTotalRepliesSelect () { | ||
357 | const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ') | ||
358 | |||
359 | // Help the planner by providing videoId that should filter out many comments | ||
360 | this.replacements.videoId = this.options.videoId | ||
361 | |||
362 | this.innerLateralJoins += `LEFT JOIN LATERAL (` + | ||
363 | `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + | ||
364 | `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + | ||
365 | `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` + | ||
366 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` + | ||
367 | `AND "deletedAt" IS NULL ` + | ||
368 | `AND ${blockWhereString} ` + | ||
369 | `) "totalReplies" ON TRUE ` | ||
370 | } | ||
371 | |||
372 | private buildAuthorTotalRepliesSelect () { | ||
373 | // Help the planner by providing videoId that should filter out many comments | ||
374 | this.replacements.videoId = this.options.videoId | ||
375 | |||
376 | this.innerLateralJoins += `LEFT JOIN LATERAL (` + | ||
377 | `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` + | ||
378 | `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` + | ||
379 | `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` + | ||
380 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` + | ||
381 | `) "totalRepliesFromVideoAuthor" ON TRUE ` | ||
382 | } | ||
383 | |||
384 | private getOrder () { | ||
385 | if (!this.options.sort) return '' | ||
386 | |||
387 | const orders = getSort(this.options.sort) | ||
388 | |||
389 | return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ') | ||
390 | } | ||
391 | |||
392 | private getInnerLimit () { | ||
393 | if (!this.options.count) return '' | ||
394 | |||
395 | this.replacements.limit = this.options.count | ||
396 | this.replacements.offset = this.options.start || 0 | ||
397 | |||
398 | return `LIMIT :limit OFFSET :offset ` | ||
399 | } | ||
400 | } | ||
diff --git a/server/models/video/sql/comment/video-comment-table-attributes.ts b/server/models/video/sql/comment/video-comment-table-attributes.ts new file mode 100644 index 000000000..87f8750c1 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-table-attributes.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { Memoize } from '@server/helpers/memoize' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { ServerModel } from '@server/models/server/server' | ||
6 | import { VideoCommentModel } from '../../video-comment' | ||
7 | |||
8 | export class VideoCommentTableAttributes { | ||
9 | |||
10 | @Memoize() | ||
11 | getVideoCommentAttributes () { | ||
12 | return VideoCommentModel.getSQLAttributes('VideoCommentModel').join(', ') | ||
13 | } | ||
14 | |||
15 | @Memoize() | ||
16 | getAccountAttributes () { | ||
17 | return AccountModel.getSQLAttributes('Account', 'Account.').join(', ') | ||
18 | } | ||
19 | |||
20 | @Memoize() | ||
21 | getVideoAttributes () { | ||
22 | return [ | ||
23 | `"Video"."id" AS "Video.id"`, | ||
24 | `"Video"."uuid" AS "Video.uuid"`, | ||
25 | `"Video"."name" AS "Video.name"` | ||
26 | ].join(', ') | ||
27 | } | ||
28 | |||
29 | @Memoize() | ||
30 | getActorAttributes () { | ||
31 | return ActorModel.getSQLAPIAttributes('Account->Actor', `Account.Actor.`).join(', ') | ||
32 | } | ||
33 | |||
34 | @Memoize() | ||
35 | getServerAttributes () { | ||
36 | return ServerModel.getSQLAttributes('Account->Actor->Server', `Account.Actor.Server.`).join(', ') | ||
37 | } | ||
38 | |||
39 | @Memoize() | ||
40 | getAvatarAttributes () { | ||
41 | return ActorImageModel.getSQLAttributes('Account->Actor->Avatars', 'Account.Actor.Avatars.').join(', ') | ||
42 | } | ||
43 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index f0ce69501..cbd57ad8c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { createSafeIn } from '@server/models/utils' | ||
4 | import { MUserAccountId } from '@server/types/models' | 3 | import { MUserAccountId } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
6 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' | 5 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' |
6 | import { createSafeIn } from '../../../../shared' | ||
7 | import { VideoTableAttributes } from './video-table-attributes' | 7 | import { VideoTableAttributes } from './video-table-attributes' |
8 | 8 | ||
9 | /** | 9 | /** |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 7c864bf27..62f1855c7 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -2,11 +2,12 @@ import { Sequelize, Transaction } from 'sequelize' | |||
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { exists } from '@server/helpers/custom-validators/misc' | 3 | import { exists } from '@server/helpers/custom-validators/misc' |
4 | import { WEBSERVER } from '@server/initializers/constants' | 4 | import { WEBSERVER } from '@server/initializers/constants' |
5 | import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' | 5 | import { buildSortDirectionAndField } from '@server/models/shared' |
6 | import { MUserAccountId, MUserId } from '@server/types/models' | 6 | import { MUserAccountId, MUserId } from '@server/types/models' |
7 | import { forceNumber } from '@shared/core-utils' | ||
7 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' | 8 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' |
9 | import { createSafeIn, parseRowCountResult } from '../../../shared' | ||
8 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' | 10 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' |
9 | import { forceNumber } from '@shared/core-utils' | ||
10 | 11 | ||
11 | /** | 12 | /** |
12 | * | 13 | * |
@@ -665,7 +666,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
665 | } | 666 | } |
666 | 667 | ||
667 | private buildOrder (value: string) { | 668 | private buildOrder (value: string) { |
668 | const { direction, field } = buildDirectionAndField(value) | 669 | const { direction, field } = buildSortDirectionAndField(value) |
669 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | 670 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) |
670 | 671 | ||
671 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | 672 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' |
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 653b9694b..cebde3755 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -4,7 +4,7 @@ import { MTag } from '@server/types/models' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' | 5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' |
6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' | 6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../shared' |
8 | import { VideoModel } from './video' | 8 | import { VideoModel } from './video' |
9 | import { VideoTagModel } from './video-tag' | 9 | import { VideoTagModel } from './video-tag' |
10 | 10 | ||
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 1cd8224c0..9247d0e2b 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -5,7 +5,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
8 | import { getBlacklistSort, searchAttribute, SortType, throwIfNotValid } from '../utils' | 8 | import { getBlacklistSort, searchAttribute, throwIfNotValid } from '../shared' |
9 | import { ThumbnailModel } from './thumbnail' | 9 | import { ThumbnailModel } from './thumbnail' |
10 | import { VideoModel } from './video' | 10 | import { VideoModel } from './video' |
11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
@@ -57,7 +57,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
57 | static listForApi (parameters: { | 57 | static listForApi (parameters: { |
58 | start: number | 58 | start: number |
59 | count: number | 59 | count: number |
60 | sort: SortType | 60 | sort: string |
61 | search?: string | 61 | search?: string |
62 | type?: VideoBlacklistType | 62 | type?: VideoBlacklistType |
63 | }) { | 63 | }) { |
@@ -67,7 +67,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
67 | return { | 67 | return { |
68 | offset: start, | 68 | offset: start, |
69 | limit: count, | 69 | limit: count, |
70 | order: getBlacklistSort(sort.sortModel, sort.sortValue) | 70 | order: getBlacklistSort(sort) |
71 | } | 71 | } |
72 | } | 72 | } |
73 | 73 | ||
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 5fbcd6e3b..2eaa77407 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -23,7 +23,7 @@ import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/vid | |||
23 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { CONFIG } from '../../initializers/config' | 24 | import { CONFIG } from '../../initializers/config' |
25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' | 25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' |
26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' | 26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../shared' |
27 | import { VideoModel } from './video' | 27 | import { VideoModel } from './video' |
28 | 28 | ||
29 | export enum ScopeNames { | 29 | export enum ScopeNames { |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 1a1b8c88d..2db4b523a 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -3,7 +3,7 @@ import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@se | |||
3 | import { AttributesOnly } from '@shared/typescript-utils' | 3 | import { AttributesOnly } from '@shared/typescript-utils' |
4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | 4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' |
5 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
6 | import { getSort } from '../utils' | 6 | import { getSort } from '../shared' |
7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' |
8 | 8 | ||
9 | enum ScopeNames { | 9 | enum ScopeNames { |
diff --git a/server/models/video/video-channel-sync.ts b/server/models/video/video-channel-sync.ts index 6e49cde10..a4cbf51f5 100644 --- a/server/models/video/video-channel-sync.ts +++ b/server/models/video/video-channel-sync.ts | |||
@@ -21,7 +21,7 @@ import { VideoChannelSync, VideoChannelSyncState } from '@shared/models' | |||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 21 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { AccountModel } from '../account/account' | 22 | import { AccountModel } from '../account/account' |
23 | import { UserModel } from '../user/user' | 23 | import { UserModel } from '../user/user' |
24 | import { getChannelSyncSort, throwIfNotValid } from '../utils' | 24 | import { getChannelSyncSort, throwIfNotValid } from '../shared' |
25 | import { VideoChannelModel } from './video-channel' | 25 | import { VideoChannelModel } from './video-channel' |
26 | 26 | ||
27 | @DefaultScope(() => ({ | 27 | @DefaultScope(() => ({ |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 132c8f021..b71f5a197 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -43,8 +43,14 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
43 | import { ActorFollowModel } from '../actor/actor-follow' | 43 | import { ActorFollowModel } from '../actor/actor-follow' |
44 | import { ActorImageModel } from '../actor/actor-image' | 44 | import { ActorImageModel } from '../actor/actor-image' |
45 | import { ServerModel } from '../server/server' | 45 | import { ServerModel } from '../server/server' |
46 | import { setAsUpdated } from '../shared' | 46 | import { |
47 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 47 | buildServerIdsFollowedBy, |
48 | buildTrigramSearchIndex, | ||
49 | createSimilarityAttribute, | ||
50 | getSort, | ||
51 | setAsUpdated, | ||
52 | throwIfNotValid | ||
53 | } from '../shared' | ||
48 | import { VideoModel } from './video' | 54 | import { VideoModel } from './video' |
49 | import { VideoPlaylistModel } from './video-playlist' | 55 | import { VideoPlaylistModel } from './video-playlist' |
50 | 56 | ||
@@ -831,6 +837,6 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
831 | } | 837 | } |
832 | 838 | ||
833 | setAsUpdated (transaction?: Transaction) { | 839 | setAsUpdated (transaction?: Transaction) { |
834 | return setAsUpdated('videoChannel', this.id, transaction) | 840 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoChannel', id: this.id, transaction }) |
835 | } | 841 | } |
836 | } | 842 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index af9614d30..ff5142809 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 1 | import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BelongsTo, | 4 | BelongsTo, |
@@ -13,11 +13,9 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { exists } from '@server/helpers/custom-validators/misc' | ||
17 | import { getServerActor } from '@server/models/application/application' | 16 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 17 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { uniqify } from '@shared/core-utils' | 18 | import { pick, uniqify } from '@shared/core-utils' |
20 | import { VideoPrivacy } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
23 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
@@ -41,61 +39,19 @@ import { | |||
41 | } from '../../types/models/video' | 39 | } from '../../types/models/video' |
42 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
43 | import { AccountModel } from '../account/account' | 41 | import { AccountModel } from '../account/account' |
44 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | 42 | import { ActorModel } from '../actor/actor' |
45 | import { | 43 | import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared' |
46 | buildBlockedAccountSQL, | 44 | import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder' |
47 | buildBlockedAccountSQLOptimized, | ||
48 | buildLocalAccountIdsIn, | ||
49 | getCommentSort, | ||
50 | searchAttribute, | ||
51 | throwIfNotValid | ||
52 | } from '../utils' | ||
53 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
54 | import { VideoChannelModel } from './video-channel' | 46 | import { VideoChannelModel } from './video-channel' |
55 | 47 | ||
56 | export enum ScopeNames { | 48 | export enum ScopeNames { |
57 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
58 | WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', | ||
59 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', | 50 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', |
60 | WITH_VIDEO = 'WITH_VIDEO', | 51 | WITH_VIDEO = 'WITH_VIDEO' |
61 | ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' | ||
62 | } | 52 | } |
63 | 53 | ||
64 | @Scopes(() => ({ | 54 | @Scopes(() => ({ |
65 | [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => { | ||
66 | return { | ||
67 | attributes: { | ||
68 | include: [ | ||
69 | [ | ||
70 | Sequelize.literal( | ||
71 | '(' + | ||
72 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + | ||
73 | 'SELECT COUNT("replies"."id") ' + | ||
74 | 'FROM "videoComment" AS "replies" ' + | ||
75 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
76 | 'AND "deletedAt" IS NULL ' + | ||
77 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
78 | ')' | ||
79 | ), | ||
80 | 'totalReplies' | ||
81 | ], | ||
82 | [ | ||
83 | Sequelize.literal( | ||
84 | '(' + | ||
85 | 'SELECT COUNT("replies"."id") ' + | ||
86 | 'FROM "videoComment" AS "replies" ' + | ||
87 | 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' + | ||
88 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
89 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
90 | 'AND "replies"."accountId" = "videoChannel"."accountId"' + | ||
91 | ')' | ||
92 | ), | ||
93 | 'totalRepliesFromVideoAuthor' | ||
94 | ] | ||
95 | ] | ||
96 | } | ||
97 | } as FindOptions | ||
98 | }, | ||
99 | [ScopeNames.WITH_ACCOUNT]: { | 55 | [ScopeNames.WITH_ACCOUNT]: { |
100 | include: [ | 56 | include: [ |
101 | { | 57 | { |
@@ -103,22 +59,6 @@ export enum ScopeNames { | |||
103 | } | 59 | } |
104 | ] | 60 | ] |
105 | }, | 61 | }, |
106 | [ScopeNames.WITH_ACCOUNT_FOR_API]: { | ||
107 | include: [ | ||
108 | { | ||
109 | model: AccountModel.unscoped(), | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: { | ||
113 | exclude: unusedActorAttributesForAPI | ||
114 | }, | ||
115 | model: ActorModel, // Default scope includes avatar and server | ||
116 | required: true | ||
117 | } | ||
118 | ] | ||
119 | } | ||
120 | ] | ||
121 | }, | ||
122 | [ScopeNames.WITH_IN_REPLY_TO]: { | 62 | [ScopeNames.WITH_IN_REPLY_TO]: { |
123 | include: [ | 63 | include: [ |
124 | { | 64 | { |
@@ -252,6 +192,18 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
252 | }) | 192 | }) |
253 | CommentAbuses: VideoCommentAbuseModel[] | 193 | CommentAbuses: VideoCommentAbuseModel[] |
254 | 194 | ||
195 | // --------------------------------------------------------------------------- | ||
196 | |||
197 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
198 | return buildSQLAttributes({ | ||
199 | model: this, | ||
200 | tableName, | ||
201 | aliasPrefix | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // --------------------------------------------------------------------------- | ||
206 | |||
255 | static loadById (id: number, t?: Transaction): Promise<MComment> { | 207 | static loadById (id: number, t?: Transaction): Promise<MComment> { |
256 | const query: FindOptions = { | 208 | const query: FindOptions = { |
257 | where: { | 209 | where: { |
@@ -319,93 +271,19 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
319 | searchAccount?: string | 271 | searchAccount?: string |
320 | searchVideo?: string | 272 | searchVideo?: string |
321 | }) { | 273 | }) { |
322 | const { start, count, sort, isLocal, search, searchAccount, searchVideo, onLocalVideo } = parameters | 274 | const queryOptions: ListVideoCommentsOptions = { |
275 | ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]), | ||
323 | 276 | ||
324 | const where: WhereOptions = { | 277 | selectType: 'api', |
325 | deletedAt: null | 278 | notDeleted: true |
326 | } | ||
327 | |||
328 | const whereAccount: WhereOptions = {} | ||
329 | const whereActor: WhereOptions = {} | ||
330 | const whereVideo: WhereOptions = {} | ||
331 | |||
332 | if (isLocal === true) { | ||
333 | Object.assign(whereActor, { | ||
334 | serverId: null | ||
335 | }) | ||
336 | } else if (isLocal === false) { | ||
337 | Object.assign(whereActor, { | ||
338 | serverId: { | ||
339 | [Op.ne]: null | ||
340 | } | ||
341 | }) | ||
342 | } | ||
343 | |||
344 | if (search) { | ||
345 | Object.assign(where, { | ||
346 | [Op.or]: [ | ||
347 | searchAttribute(search, 'text'), | ||
348 | searchAttribute(search, '$Account.Actor.preferredUsername$'), | ||
349 | searchAttribute(search, '$Account.name$'), | ||
350 | searchAttribute(search, '$Video.name$') | ||
351 | ] | ||
352 | }) | ||
353 | } | ||
354 | |||
355 | if (searchAccount) { | ||
356 | Object.assign(whereActor, { | ||
357 | [Op.or]: [ | ||
358 | searchAttribute(searchAccount, '$Account.Actor.preferredUsername$'), | ||
359 | searchAttribute(searchAccount, '$Account.name$') | ||
360 | ] | ||
361 | }) | ||
362 | } | ||
363 | |||
364 | if (searchVideo) { | ||
365 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | ||
366 | } | ||
367 | |||
368 | if (exists(onLocalVideo)) { | ||
369 | Object.assign(whereVideo, { remote: !onLocalVideo }) | ||
370 | } | ||
371 | |||
372 | const getQuery = (forCount: boolean) => { | ||
373 | return { | ||
374 | offset: start, | ||
375 | limit: count, | ||
376 | order: getCommentSort(sort), | ||
377 | where, | ||
378 | include: [ | ||
379 | { | ||
380 | model: AccountModel.unscoped(), | ||
381 | required: true, | ||
382 | where: whereAccount, | ||
383 | include: [ | ||
384 | { | ||
385 | attributes: { | ||
386 | exclude: unusedActorAttributesForAPI | ||
387 | }, | ||
388 | model: forCount === true | ||
389 | ? ActorModel.unscoped() // Default scope includes avatar and server | ||
390 | : ActorModel, | ||
391 | required: true, | ||
392 | where: whereActor | ||
393 | } | ||
394 | ] | ||
395 | }, | ||
396 | { | ||
397 | model: VideoModel.unscoped(), | ||
398 | required: true, | ||
399 | where: whereVideo | ||
400 | } | ||
401 | ] | ||
402 | } | ||
403 | } | 279 | } |
404 | 280 | ||
405 | return Promise.all([ | 281 | return Promise.all([ |
406 | VideoCommentModel.count(getQuery(true)), | 282 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
407 | VideoCommentModel.findAll(getQuery(false)) | 283 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
408 | ]).then(([ total, data ]) => ({ total, data })) | 284 | ]).then(([ rows, count ]) => { |
285 | return { total: count, data: rows } | ||
286 | }) | ||
409 | } | 287 | } |
410 | 288 | ||
411 | static async listThreadsForApi (parameters: { | 289 | static async listThreadsForApi (parameters: { |
@@ -416,67 +294,40 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
416 | sort: string | 294 | sort: string |
417 | user?: MUserAccountId | 295 | user?: MUserAccountId |
418 | }) { | 296 | }) { |
419 | const { videoId, isVideoOwned, start, count, sort, user } = parameters | 297 | const { videoId, user } = parameters |
420 | 298 | ||
421 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 299 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
422 | 300 | ||
423 | const accountBlockedWhere = { | 301 | const commonOptions: ListVideoCommentsOptions = { |
424 | accountId: { | 302 | selectType: 'api', |
425 | [Op.notIn]: Sequelize.literal( | 303 | videoId, |
426 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | 304 | blockerAccountIds |
427 | ) | ||
428 | } | ||
429 | } | 305 | } |
430 | 306 | ||
431 | const queryList = { | 307 | const listOptions: ListVideoCommentsOptions = { |
432 | offset: start, | 308 | ...commonOptions, |
433 | limit: count, | 309 | ...pick(parameters, [ 'sort', 'start', 'count' ]), |
434 | order: getCommentSort(sort), | 310 | |
435 | where: { | 311 | isThread: true, |
436 | [Op.and]: [ | 312 | includeReplyCounters: true |
437 | { | ||
438 | videoId | ||
439 | }, | ||
440 | { | ||
441 | inReplyToCommentId: null | ||
442 | }, | ||
443 | { | ||
444 | [Op.or]: [ | ||
445 | accountBlockedWhere, | ||
446 | { | ||
447 | accountId: null | ||
448 | } | ||
449 | ] | ||
450 | } | ||
451 | ] | ||
452 | } | ||
453 | } | 313 | } |
454 | 314 | ||
455 | const findScopesList: (string | ScopeOptions)[] = [ | 315 | const countOptions: ListVideoCommentsOptions = { |
456 | ScopeNames.WITH_ACCOUNT_FOR_API, | 316 | ...commonOptions, |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | 317 | ||
462 | const countScopesList: ScopeOptions[] = [ | 318 | isThread: true |
463 | { | 319 | } |
464 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
465 | } | ||
466 | ] | ||
467 | 320 | ||
468 | const notDeletedQueryCount = { | 321 | const notDeletedCountOptions: ListVideoCommentsOptions = { |
469 | where: { | 322 | ...commonOptions, |
470 | videoId, | 323 | |
471 | deletedAt: null, | 324 | notDeleted: true |
472 | ...accountBlockedWhere | ||
473 | } | ||
474 | } | 325 | } |
475 | 326 | ||
476 | return Promise.all([ | 327 | return Promise.all([ |
477 | VideoCommentModel.scope(findScopesList).findAll(queryList), | 328 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(), |
478 | VideoCommentModel.scope(countScopesList).count(queryList), | 329 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(), |
479 | VideoCommentModel.count(notDeletedQueryCount) | 330 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments() |
480 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | 331 | ]).then(([ rows, count, totalNotDeletedComments ]) => { |
481 | return { total: count, data: rows, totalNotDeletedComments } | 332 | return { total: count, data: rows, totalNotDeletedComments } |
482 | }) | 333 | }) |
@@ -484,54 +335,29 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
484 | 335 | ||
485 | static async listThreadCommentsForApi (parameters: { | 336 | static async listThreadCommentsForApi (parameters: { |
486 | videoId: number | 337 | videoId: number |
487 | isVideoOwned: boolean | ||
488 | threadId: number | 338 | threadId: number |
489 | user?: MUserAccountId | 339 | user?: MUserAccountId |
490 | }) { | 340 | }) { |
491 | const { videoId, threadId, user, isVideoOwned } = parameters | 341 | const { user } = parameters |
492 | 342 | ||
493 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 343 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
494 | 344 | ||
495 | const query = { | 345 | const queryOptions: ListVideoCommentsOptions = { |
496 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, | 346 | ...pick(parameters, [ 'videoId', 'threadId' ]), |
497 | where: { | ||
498 | videoId, | ||
499 | [Op.and]: [ | ||
500 | { | ||
501 | [Op.or]: [ | ||
502 | { id: threadId }, | ||
503 | { originCommentId: threadId } | ||
504 | ] | ||
505 | }, | ||
506 | { | ||
507 | [Op.or]: [ | ||
508 | { | ||
509 | accountId: { | ||
510 | [Op.notIn]: Sequelize.literal( | ||
511 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
512 | ) | ||
513 | } | ||
514 | }, | ||
515 | { | ||
516 | accountId: null | ||
517 | } | ||
518 | ] | ||
519 | } | ||
520 | ] | ||
521 | } | ||
522 | } | ||
523 | 347 | ||
524 | const scopes: any[] = [ | 348 | selectType: 'api', |
525 | ScopeNames.WITH_ACCOUNT_FOR_API, | 349 | sort: 'createdAt', |
526 | { | 350 | |
527 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 351 | blockerAccountIds, |
528 | } | 352 | includeReplyCounters: true |
529 | ] | 353 | } |
530 | 354 | ||
531 | return Promise.all([ | 355 | return Promise.all([ |
532 | VideoCommentModel.count(query), | 356 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
533 | VideoCommentModel.scope(scopes).findAll(query) | 357 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
534 | ]).then(([ total, data ]) => ({ total, data })) | 358 | ]).then(([ rows, count ]) => { |
359 | return { total: count, data: rows } | ||
360 | }) | ||
535 | } | 361 | } |
536 | 362 | ||
537 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 363 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -559,31 +385,31 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
559 | .findAll(query) | 385 | .findAll(query) |
560 | } | 386 | } |
561 | 387 | ||
562 | static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { | 388 | static async listAndCountByVideoForAP (parameters: { |
563 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ | 389 | video: MVideoImmutable |
390 | start: number | ||
391 | count: number | ||
392 | }) { | ||
393 | const { video } = parameters | ||
394 | |||
395 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) | ||
396 | |||
397 | const queryOptions: ListVideoCommentsOptions = { | ||
398 | ...pick(parameters, [ 'start', 'count' ]), | ||
399 | |||
400 | selectType: 'comment-only', | ||
564 | videoId: video.id, | 401 | videoId: video.id, |
565 | isVideoOwned: video.isOwned() | 402 | sort: 'createdAt', |
566 | }) | ||
567 | 403 | ||
568 | const query = { | 404 | blockerAccountIds |
569 | order: [ [ 'createdAt', 'ASC' ] ] as Order, | ||
570 | offset: start, | ||
571 | limit: count, | ||
572 | where: { | ||
573 | videoId: video.id, | ||
574 | accountId: { | ||
575 | [Op.notIn]: Sequelize.literal( | ||
576 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
577 | ) | ||
578 | } | ||
579 | }, | ||
580 | transaction: t | ||
581 | } | 405 | } |
582 | 406 | ||
583 | return Promise.all([ | 407 | return Promise.all([ |
584 | VideoCommentModel.count(query), | 408 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(), |
585 | VideoCommentModel.findAll<MComment>(query) | 409 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
586 | ]).then(([ total, data ]) => ({ total, data })) | 410 | ]).then(([ rows, count ]) => { |
411 | return { total: count, data: rows } | ||
412 | }) | ||
587 | } | 413 | } |
588 | 414 | ||
589 | static async listForFeed (parameters: { | 415 | static async listForFeed (parameters: { |
@@ -592,97 +418,36 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
592 | videoId?: number | 418 | videoId?: number |
593 | accountId?: number | 419 | accountId?: number |
594 | videoChannelId?: number | 420 | videoChannelId?: number |
595 | }): Promise<MCommentOwnerVideoFeed[]> { | 421 | }) { |
596 | const serverActor = await getServerActor() | 422 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) |
597 | const { start, count, videoId, accountId, videoChannelId } = parameters | ||
598 | |||
599 | const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized( | ||
600 | '"VideoCommentModel"."accountId"', | ||
601 | [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ] | ||
602 | ) | ||
603 | 423 | ||
604 | if (accountId) { | 424 | const queryOptions: ListVideoCommentsOptions = { |
605 | whereAnd.push({ | 425 | ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]), |
606 | accountId | ||
607 | }) | ||
608 | } | ||
609 | 426 | ||
610 | const accountWhere = { | 427 | selectType: 'feed', |
611 | [Op.and]: whereAnd | ||
612 | } | ||
613 | 428 | ||
614 | const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined | 429 | sort: '-createdAt', |
430 | onPublicVideo: true, | ||
431 | notDeleted: true, | ||
615 | 432 | ||
616 | const query = { | 433 | blockerAccountIds |
617 | order: [ [ 'createdAt', 'DESC' ] ] as Order, | ||
618 | offset: start, | ||
619 | limit: count, | ||
620 | where: { | ||
621 | deletedAt: null, | ||
622 | accountId: accountWhere | ||
623 | }, | ||
624 | include: [ | ||
625 | { | ||
626 | attributes: [ 'name', 'uuid' ], | ||
627 | model: VideoModel.unscoped(), | ||
628 | required: true, | ||
629 | where: { | ||
630 | privacy: VideoPrivacy.PUBLIC | ||
631 | }, | ||
632 | include: [ | ||
633 | { | ||
634 | attributes: [ 'accountId' ], | ||
635 | model: VideoChannelModel.unscoped(), | ||
636 | required: true, | ||
637 | where: videoChannelWhere | ||
638 | } | ||
639 | ] | ||
640 | } | ||
641 | ] | ||
642 | } | 434 | } |
643 | 435 | ||
644 | if (videoId) query.where['videoId'] = videoId | 436 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>() |
645 | |||
646 | return VideoCommentModel | ||
647 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
648 | .findAll(query) | ||
649 | } | 437 | } |
650 | 438 | ||
651 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { | 439 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { |
652 | const accountWhere = filter.onVideosOfAccount | 440 | const queryOptions: ListVideoCommentsOptions = { |
653 | ? { id: filter.onVideosOfAccount.id } | 441 | selectType: 'comment-only', |
654 | : {} | ||
655 | 442 | ||
656 | const query = { | 443 | accountId: ofAccount.id, |
657 | limit: 1000, | 444 | videoAccountOwnerId: filter.onVideosOfAccount?.id, |
658 | where: { | 445 | |
659 | deletedAt: null, | 446 | notDeleted: true, |
660 | accountId: ofAccount.id | 447 | count: 5000 |
661 | }, | ||
662 | include: [ | ||
663 | { | ||
664 | model: VideoModel, | ||
665 | required: true, | ||
666 | include: [ | ||
667 | { | ||
668 | model: VideoChannelModel, | ||
669 | required: true, | ||
670 | include: [ | ||
671 | { | ||
672 | model: AccountModel, | ||
673 | required: true, | ||
674 | where: accountWhere | ||
675 | } | ||
676 | ] | ||
677 | } | ||
678 | ] | ||
679 | } | ||
680 | ] | ||
681 | } | 448 | } |
682 | 449 | ||
683 | return VideoCommentModel | 450 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>() |
684 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
685 | .findAll(query) | ||
686 | } | 451 | } |
687 | 452 | ||
688 | static async getStats () { | 453 | static async getStats () { |
@@ -750,9 +515,7 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
750 | } | 515 | } |
751 | 516 | ||
752 | isOwned () { | 517 | isOwned () { |
753 | if (!this.Account) { | 518 | if (!this.Account) return false |
754 | return false | ||
755 | } | ||
756 | 519 | ||
757 | return this.Account.isOwned() | 520 | return this.Account.isOwned() |
758 | } | 521 | } |
@@ -906,22 +669,15 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
906 | } | 669 | } |
907 | 670 | ||
908 | private static async buildBlockerAccountIds (options: { | 671 | private static async buildBlockerAccountIds (options: { |
909 | videoId: number | 672 | user: MUserAccountId |
910 | isVideoOwned: boolean | 673 | }): Promise<number[]> { |
911 | user?: MUserAccountId | 674 | const { user } = options |
912 | }) { | ||
913 | const { videoId, user, isVideoOwned } = options | ||
914 | 675 | ||
915 | const serverActor = await getServerActor() | 676 | const serverActor = await getServerActor() |
916 | const blockerAccountIds = [ serverActor.Account.id ] | 677 | const blockerAccountIds = [ serverActor.Account.id ] |
917 | 678 | ||
918 | if (user) blockerAccountIds.push(user.Account.id) | 679 | if (user) blockerAccountIds.push(user.Account.id) |
919 | 680 | ||
920 | if (isVideoOwned) { | ||
921 | const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId) | ||
922 | if (videoOwnerAccount) blockerAccountIds.push(videoOwnerAccount.id) | ||
923 | } | ||
924 | |||
925 | return blockerAccountIds | 681 | return blockerAccountIds |
926 | } | 682 | } |
927 | } | 683 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 9c4e6d078..07bc13de1 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | import validator from 'validator' | 21 | import validator from 'validator' |
22 | import { logger } from '@server/helpers/logger' | 22 | import { logger } from '@server/helpers/logger' |
23 | import { extractVideo } from '@server/helpers/video' | 23 | import { extractVideo } from '@server/helpers/video' |
24 | import { CONFIG } from '@server/initializers/config' | ||
24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | 25 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' |
25 | import { | 26 | import { |
26 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
@@ -50,11 +51,9 @@ import { | |||
50 | } from '../../initializers/constants' | 51 | } from '../../initializers/constants' |
51 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | 52 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' |
52 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 53 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
53 | import { doesExist } from '../shared' | 54 | import { doesExist, parseAggregateResult, throwIfNotValid } from '../shared' |
54 | import { parseAggregateResult, throwIfNotValid } from '../utils' | ||
55 | import { VideoModel } from './video' | 55 | import { VideoModel } from './video' |
56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
57 | import { CONFIG } from '@server/initializers/config' | ||
58 | 57 | ||
59 | export enum ScopeNames { | 58 | export enum ScopeNames { |
60 | WITH_VIDEO = 'WITH_VIDEO', | 59 | WITH_VIDEO = 'WITH_VIDEO', |
@@ -266,7 +265,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | static doesInfohashExist (infoHash: string) { | 265 | static doesInfohashExist (infoHash: string) { |
267 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 266 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
268 | 267 | ||
269 | return doesExist(query, { infoHash }) | 268 | return doesExist(this.sequelize, query, { infoHash }) |
270 | } | 269 | } |
271 | 270 | ||
272 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 271 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -282,14 +281,14 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
282 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
283 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' |
284 | 283 | ||
285 | return doesExist(query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
286 | } | 285 | } |
287 | 286 | ||
288 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { |
289 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
290 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
291 | 290 | ||
292 | return doesExist(query, { filename }) | 291 | return doesExist(this.sequelize, query, { filename }) |
293 | } | 292 | } |
294 | 293 | ||
295 | static loadByFilename (filename: string) { | 294 | static loadByFilename (filename: string) { |
@@ -439,7 +438,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
439 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
440 | 439 | ||
441 | for (const k of Object.keys(videoFile.toJSON())) { | 440 | for (const k of Object.keys(videoFile.toJSON())) { |
442 | element[k] = videoFile[k] | 441 | element.set(k, videoFile[k]) |
443 | } | 442 | } |
444 | 443 | ||
445 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index da6b92c7a..c040e0fda 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -22,7 +22,7 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help | |||
22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' | 22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' |
23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
24 | import { UserModel } from '../user/user' | 24 | import { UserModel } from '../user/user' |
25 | import { getSort, searchAttribute, throwIfNotValid } from '../utils' | 25 | import { getSort, searchAttribute, throwIfNotValid } from '../shared' |
26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
27 | import { VideoChannelSyncModel } from './video-channel-sync' | 27 | import { VideoChannelSyncModel } from './video-channel-sync' |
28 | 28 | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index 7181b5599..b832f9768 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -31,7 +31,7 @@ import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/ | |||
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
33 | import { AccountModel } from '../account/account' | 33 | import { AccountModel } from '../account/account' |
34 | import { getSort, throwIfNotValid } from '../utils' | 34 | import { getSort, throwIfNotValid } from '../shared' |
35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
36 | import { VideoPlaylistModel } from './video-playlist' | 36 | import { VideoPlaylistModel } from './video-playlist' |
37 | 37 | ||
@@ -309,7 +309,23 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
309 | return VideoPlaylistElementModel.increment({ position: by }, query) | 309 | return VideoPlaylistElementModel.increment({ position: by }, query) |
310 | } | 310 | } |
311 | 311 | ||
312 | getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { | 312 | toFormattedJSON ( |
313 | this: MVideoPlaylistElementFormattable, | ||
314 | options: { accountId?: number } = {} | ||
315 | ): VideoPlaylistElement { | ||
316 | return { | ||
317 | id: this.id, | ||
318 | position: this.position, | ||
319 | startTimestamp: this.startTimestamp, | ||
320 | stopTimestamp: this.stopTimestamp, | ||
321 | |||
322 | type: this.getType(options.accountId), | ||
323 | |||
324 | video: this.getVideoElement(options.accountId) | ||
325 | } | ||
326 | } | ||
327 | |||
328 | getType (this: MVideoPlaylistElementFormattable, accountId?: number) { | ||
313 | const video = this.Video | 329 | const video = this.Video |
314 | 330 | ||
315 | if (!video) return VideoPlaylistElementType.DELETED | 331 | if (!video) return VideoPlaylistElementType.DELETED |
@@ -323,34 +339,17 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
323 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE | 339 | if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE |
324 | 340 | ||
325 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | 341 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE |
326 | if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE | ||
327 | 342 | ||
328 | return VideoPlaylistElementType.REGULAR | 343 | return VideoPlaylistElementType.REGULAR |
329 | } | 344 | } |
330 | 345 | ||
331 | getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { | 346 | getVideoElement (this: MVideoPlaylistElementFormattable, accountId?: number) { |
332 | if (!this.Video) return null | 347 | if (!this.Video) return null |
333 | if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null | 348 | if (this.getType(accountId) !== VideoPlaylistElementType.REGULAR) return null |
334 | 349 | ||
335 | return this.Video.toFormattedJSON() | 350 | return this.Video.toFormattedJSON() |
336 | } | 351 | } |
337 | 352 | ||
338 | toFormattedJSON ( | ||
339 | this: MVideoPlaylistElementFormattable, | ||
340 | options: { displayNSFW?: boolean, accountId?: number } = {} | ||
341 | ): VideoPlaylistElement { | ||
342 | return { | ||
343 | id: this.id, | ||
344 | position: this.position, | ||
345 | startTimestamp: this.startTimestamp, | ||
346 | stopTimestamp: this.stopTimestamp, | ||
347 | |||
348 | type: this.getType(options.displayNSFW, options.accountId), | ||
349 | |||
350 | video: this.getVideoElement(options.displayNSFW, options.accountId) | ||
351 | } | ||
352 | } | ||
353 | |||
354 | toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject { | 353 | toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject { |
355 | const base: PlaylistElementObject = { | 354 | const base: PlaylistElementObject = { |
356 | id: this.url, | 355 | id: this.url, |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 8bbe54c49..faf4bea78 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -21,12 +21,8 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect | |||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' | 22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' |
23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
24 | import { ActivityIconObject, PlaylistObject, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models' | ||
24 | import { AttributesOnly } from '@shared/typescript-utils' | 25 | import { AttributesOnly } from '@shared/typescript-utils' |
25 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
26 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
27 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
28 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | ||
29 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | ||
30 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 26 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
31 | import { | 27 | import { |
32 | isVideoPlaylistDescriptionValid, | 28 | isVideoPlaylistDescriptionValid, |
@@ -53,7 +49,6 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 49 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 50 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 51 | import { ActorModel } from '../actor/actor' |
56 | import { setAsUpdated } from '../shared' | ||
57 | import { | 52 | import { |
58 | buildServerIdsFollowedBy, | 53 | buildServerIdsFollowedBy, |
59 | buildTrigramSearchIndex, | 54 | buildTrigramSearchIndex, |
@@ -61,8 +56,9 @@ import { | |||
61 | createSimilarityAttribute, | 56 | createSimilarityAttribute, |
62 | getPlaylistSort, | 57 | getPlaylistSort, |
63 | isOutdated, | 58 | isOutdated, |
59 | setAsUpdated, | ||
64 | throwIfNotValid | 60 | throwIfNotValid |
65 | } from '../utils' | 61 | } from '../shared' |
66 | import { ThumbnailModel } from './thumbnail' | 62 | import { ThumbnailModel } from './thumbnail' |
67 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 63 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
68 | import { VideoPlaylistElementModel } from './video-playlist-element' | 64 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -641,7 +637,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
641 | } | 637 | } |
642 | 638 | ||
643 | setAsRefreshed () { | 639 | setAsRefreshed () { |
644 | return setAsUpdated('videoPlaylist', this.id) | 640 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoPlaylist', id: this.id }) |
645 | } | 641 | } |
646 | 642 | ||
647 | setVideosLength (videosLength: number) { | 643 | setVideosLength (videosLength: number) { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f2190037e..b4de2b20f 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -7,7 +7,7 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | |||
7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' | 7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' |
8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' | 8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' |
9 | import { ActorModel } from '../actor/actor' | 9 | import { ActorModel } from '../actor/actor' |
10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' | 10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../shared' |
11 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
12 | 12 | ||
13 | enum ScopeNames { | 13 | enum ScopeNames { |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 0386edf28..a85c79c9f 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -37,8 +37,7 @@ import { | |||
37 | WEBSERVER | 37 | WEBSERVER |
38 | } from '../../initializers/constants' | 38 | } from '../../initializers/constants' |
39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
40 | import { doesExist } from '../shared' | 40 | import { doesExist, throwIfNotValid } from '../shared' |
41 | import { throwIfNotValid } from '../utils' | ||
42 | import { VideoModel } from './video' | 41 | import { VideoModel } from './video' |
43 | 42 | ||
44 | @Table({ | 43 | @Table({ |
@@ -138,7 +137,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
138 | static doesInfohashExist (infoHash: string) { | 137 | static doesInfohashExist (infoHash: string) { |
139 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 138 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
140 | 139 | ||
141 | return doesExist(query, { infoHash }) | 140 | return doesExist(this.sequelize, query, { infoHash }) |
142 | } | 141 | } |
143 | 142 | ||
144 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 143 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -237,7 +236,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
237 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + | 236 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + |
238 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 237 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
239 | 238 | ||
240 | return doesExist(query, { videoUUID }) | 239 | return doesExist(this.sequelize, query, { videoUUID }) |
241 | } | 240 | } |
242 | 241 | ||
243 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { | 242 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 56cc45cfe..1a10d2da2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -32,7 +32,7 @@ import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFil | |||
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 32 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
34 | import { getServerActor } from '@server/models/application/application' | 34 | import { getServerActor } from '@server/models/application/application' |
35 | import { ModelCache } from '@server/models/model-cache' | 35 | import { ModelCache } from '@server/models/shared/model-cache' |
36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' | 37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' |
38 | import { | 38 | import { |
@@ -103,10 +103,9 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' | |||
103 | import { ServerModel } from '../server/server' | 103 | import { ServerModel } from '../server/server' |
104 | import { TrackerModel } from '../server/tracker' | 104 | import { TrackerModel } from '../server/tracker' |
105 | import { VideoTrackerModel } from '../server/video-tracker' | 105 | import { VideoTrackerModel } from '../server/video-tracker' |
106 | import { setAsUpdated } from '../shared' | 106 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, setAsUpdated, throwIfNotValid } from '../shared' |
107 | import { UserModel } from '../user/user' | 107 | import { UserModel } from '../user/user' |
108 | import { UserVideoHistoryModel } from '../user/user-video-history' | 108 | import { UserVideoHistoryModel } from '../user/user-video-history' |
109 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
110 | import { VideoViewModel } from '../view/video-view' | 109 | import { VideoViewModel } from '../view/video-view' |
111 | import { | 110 | import { |
112 | videoFilesModelToFormattedJSON, | 111 | videoFilesModelToFormattedJSON, |
@@ -1871,7 +1870,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1871 | } | 1870 | } |
1872 | 1871 | ||
1873 | setAsRefreshed (transaction?: Transaction) { | 1872 | setAsRefreshed (transaction?: Transaction) { |
1874 | return setAsUpdated('video', this.id, transaction) | 1873 | return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) |
1875 | } | 1874 | } |
1876 | 1875 | ||
1877 | // --------------------------------------------------------------------------- | 1876 | // --------------------------------------------------------------------------- |
diff --git a/server/models/view/local-video-viewer.ts b/server/models/view/local-video-viewer.ts index 9d0d89a59..274117e86 100644 --- a/server/models/view/local-video-viewer.ts +++ b/server/models/view/local-video-viewer.ts | |||
@@ -21,6 +21,10 @@ import { LocalVideoViewerWatchSectionModel } from './local-video-viewer-watch-se | |||
21 | indexes: [ | 21 | indexes: [ |
22 | { | 22 | { |
23 | fields: [ 'videoId' ] | 23 | fields: [ 'videoId' ] |
24 | }, | ||
25 | { | ||
26 | fields: [ 'url' ], | ||
27 | unique: true | ||
24 | } | 28 | } |
25 | ] | 29 | ] |
26 | }) | 30 | }) |