diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account-blocklist.ts | 31 | ||||
-rw-r--r-- | server/models/account/account.ts | 25 | ||||
-rw-r--r-- | server/models/account/user-notification-setting.ts | 150 | ||||
-rw-r--r-- | server/models/account/user-notification.ts | 472 | ||||
-rw-r--r-- | server/models/account/user-video-history.ts | 33 | ||||
-rw-r--r-- | server/models/account/user.ts | 180 | ||||
-rw-r--r-- | server/models/activitypub/actor-follow.ts | 58 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 1 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 10 | ||||
-rw-r--r-- | server/models/utils.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-blacklist.ts | 31 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 25 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 45 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 26 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-import.ts | 4 | ||||
-rw-r--r-- | server/models/video/video.ts | 112 |
18 files changed, 1110 insertions, 129 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fa2819235..efd6ed59e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated | |||
2 | import { AccountModel } from './account' | 2 | import { AccountModel } from './account' |
3 | import { getSort } from '../utils' | 3 | import { getSort } from '../utils' |
4 | import { AccountBlock } from '../../../shared/models/blocklist' | 4 | import { AccountBlock } from '../../../shared/models/blocklist' |
5 | import { Op } from 'sequelize' | ||
5 | 6 | ||
6 | enum ScopeNames { | 7 | enum ScopeNames { |
7 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | 8 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' |
@@ -72,6 +73,36 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | |||
72 | }) | 73 | }) |
73 | BlockedAccount: AccountModel | 74 | BlockedAccount: AccountModel |
74 | 75 | ||
76 | static isAccountMutedBy (accountId: number, targetAccountId: number) { | ||
77 | return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId) | ||
78 | .then(result => result[accountId]) | ||
79 | } | ||
80 | |||
81 | static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { | ||
82 | const query = { | ||
83 | attributes: [ 'accountId', 'id' ], | ||
84 | where: { | ||
85 | accountId: { | ||
86 | [Op.any]: accountIds | ||
87 | }, | ||
88 | targetAccountId | ||
89 | }, | ||
90 | raw: true | ||
91 | } | ||
92 | |||
93 | return AccountBlocklistModel.unscoped() | ||
94 | .findAll(query) | ||
95 | .then(rows => { | ||
96 | const result: { [accountId: number]: boolean } = {} | ||
97 | |||
98 | for (const accountId of accountIds) { | ||
99 | result[accountId] = !!rows.find(r => r.accountId === accountId) | ||
100 | } | ||
101 | |||
102 | return result | ||
103 | }) | ||
104 | } | ||
105 | |||
75 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { | 106 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { |
76 | const query = { | 107 | const query = { |
77 | where: { | 108 | where: { |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 5a237d733..84ef0b30d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> { | |||
241 | }) | 241 | }) |
242 | } | 242 | } |
243 | 243 | ||
244 | static listLocalsForSitemap (sort: string) { | ||
245 | const query = { | ||
246 | attributes: [ ], | ||
247 | offset: 0, | ||
248 | order: getSort(sort), | ||
249 | include: [ | ||
250 | { | ||
251 | attributes: [ 'preferredUsername', 'serverId' ], | ||
252 | model: ActorModel.unscoped(), | ||
253 | where: { | ||
254 | serverId: null | ||
255 | } | ||
256 | } | ||
257 | ] | ||
258 | } | ||
259 | |||
260 | return AccountModel | ||
261 | .unscoped() | ||
262 | .findAll(query) | ||
263 | } | ||
264 | |||
244 | toFormattedJSON (): Account { | 265 | toFormattedJSON (): Account { |
245 | const actor = this.Actor.toFormattedJSON() | 266 | const actor = this.Actor.toFormattedJSON() |
246 | const account = { | 267 | const account = { |
@@ -267,6 +288,10 @@ export class AccountModel extends Model<AccountModel> { | |||
267 | return this.Actor.isOwned() | 288 | return this.Actor.isOwned() |
268 | } | 289 | } |
269 | 290 | ||
291 | isOutdated () { | ||
292 | return this.Actor.isOutdated() | ||
293 | } | ||
294 | |||
270 | getDisplayName () { | 295 | getDisplayName () { |
271 | return this.name | 296 | return this.name |
272 | } | 297 | } |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts new file mode 100644 index 000000000..f1c3ac223 --- /dev/null +++ b/server/models/account/user-notification-setting.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import { | ||
2 | AfterDestroy, | ||
3 | AfterUpdate, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { throwIfNotValid } from '../utils' | ||
16 | import { UserModel } from './user' | ||
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
19 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
20 | |||
21 | @Table({ | ||
22 | tableName: 'userNotificationSetting', | ||
23 | indexes: [ | ||
24 | { | ||
25 | fields: [ 'userId' ], | ||
26 | unique: true | ||
27 | } | ||
28 | ] | ||
29 | }) | ||
30 | export class UserNotificationSettingModel extends Model<UserNotificationSettingModel> { | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Default(null) | ||
34 | @Is( | ||
35 | 'UserNotificationSettingNewVideoFromSubscription', | ||
36 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') | ||
37 | ) | ||
38 | @Column | ||
39 | newVideoFromSubscription: UserNotificationSettingValue | ||
40 | |||
41 | @AllowNull(false) | ||
42 | @Default(null) | ||
43 | @Is( | ||
44 | 'UserNotificationSettingNewCommentOnMyVideo', | ||
45 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') | ||
46 | ) | ||
47 | @Column | ||
48 | newCommentOnMyVideo: UserNotificationSettingValue | ||
49 | |||
50 | @AllowNull(false) | ||
51 | @Default(null) | ||
52 | @Is( | ||
53 | 'UserNotificationSettingVideoAbuseAsModerator', | ||
54 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') | ||
55 | ) | ||
56 | @Column | ||
57 | videoAbuseAsModerator: UserNotificationSettingValue | ||
58 | |||
59 | @AllowNull(false) | ||
60 | @Default(null) | ||
61 | @Is( | ||
62 | 'UserNotificationSettingBlacklistOnMyVideo', | ||
63 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') | ||
64 | ) | ||
65 | @Column | ||
66 | blacklistOnMyVideo: UserNotificationSettingValue | ||
67 | |||
68 | @AllowNull(false) | ||
69 | @Default(null) | ||
70 | @Is( | ||
71 | 'UserNotificationSettingMyVideoPublished', | ||
72 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') | ||
73 | ) | ||
74 | @Column | ||
75 | myVideoPublished: UserNotificationSettingValue | ||
76 | |||
77 | @AllowNull(false) | ||
78 | @Default(null) | ||
79 | @Is( | ||
80 | 'UserNotificationSettingMyVideoImportFinished', | ||
81 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') | ||
82 | ) | ||
83 | @Column | ||
84 | myVideoImportFinished: UserNotificationSettingValue | ||
85 | |||
86 | @AllowNull(false) | ||
87 | @Default(null) | ||
88 | @Is( | ||
89 | 'UserNotificationSettingNewUserRegistration', | ||
90 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') | ||
91 | ) | ||
92 | @Column | ||
93 | newUserRegistration: UserNotificationSettingValue | ||
94 | |||
95 | @AllowNull(false) | ||
96 | @Default(null) | ||
97 | @Is( | ||
98 | 'UserNotificationSettingNewFollow', | ||
99 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') | ||
100 | ) | ||
101 | @Column | ||
102 | newFollow: UserNotificationSettingValue | ||
103 | |||
104 | @AllowNull(false) | ||
105 | @Default(null) | ||
106 | @Is( | ||
107 | 'UserNotificationSettingCommentMention', | ||
108 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') | ||
109 | ) | ||
110 | @Column | ||
111 | commentMention: UserNotificationSettingValue | ||
112 | |||
113 | @ForeignKey(() => UserModel) | ||
114 | @Column | ||
115 | userId: number | ||
116 | |||
117 | @BelongsTo(() => UserModel, { | ||
118 | foreignKey: { | ||
119 | allowNull: false | ||
120 | }, | ||
121 | onDelete: 'cascade' | ||
122 | }) | ||
123 | User: UserModel | ||
124 | |||
125 | @CreatedAt | ||
126 | createdAt: Date | ||
127 | |||
128 | @UpdatedAt | ||
129 | updatedAt: Date | ||
130 | |||
131 | @AfterUpdate | ||
132 | @AfterDestroy | ||
133 | static removeTokenCache (instance: UserNotificationSettingModel) { | ||
134 | return clearCacheByUserId(instance.userId) | ||
135 | } | ||
136 | |||
137 | toFormattedJSON (): UserNotificationSetting { | ||
138 | return { | ||
139 | newCommentOnMyVideo: this.newCommentOnMyVideo, | ||
140 | newVideoFromSubscription: this.newVideoFromSubscription, | ||
141 | videoAbuseAsModerator: this.videoAbuseAsModerator, | ||
142 | blacklistOnMyVideo: this.blacklistOnMyVideo, | ||
143 | myVideoPublished: this.myVideoPublished, | ||
144 | myVideoImportFinished: this.myVideoImportFinished, | ||
145 | newUserRegistration: this.newUserRegistration, | ||
146 | commentMention: this.commentMention, | ||
147 | newFollow: this.newFollow | ||
148 | } | ||
149 | } | ||
150 | } | ||
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts new file mode 100644 index 000000000..6cdbb827b --- /dev/null +++ b/server/models/account/user-notification.ts | |||
@@ -0,0 +1,472 @@ | |||
1 | import { | ||
2 | AllowNull, | ||
3 | BelongsTo, | ||
4 | Column, | ||
5 | CreatedAt, | ||
6 | Default, | ||
7 | ForeignKey, | ||
8 | IFindOptions, | ||
9 | Is, | ||
10 | Model, | ||
11 | Scopes, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { UserNotification, UserNotificationType } from '../../../shared' | ||
16 | import { getSort, throwIfNotValid } from '../utils' | ||
17 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | ||
18 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | ||
19 | import { UserModel } from './user' | ||
20 | import { VideoModel } from '../video/video' | ||
21 | import { VideoCommentModel } from '../video/video-comment' | ||
22 | import { Op } from 'sequelize' | ||
23 | import { VideoChannelModel } from '../video/video-channel' | ||
24 | import { AccountModel } from './account' | ||
25 | import { VideoAbuseModel } from '../video/video-abuse' | ||
26 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
27 | import { VideoImportModel } from '../video/video-import' | ||
28 | import { ActorModel } from '../activitypub/actor' | ||
29 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
30 | import { AvatarModel } from '../avatar/avatar' | ||
31 | import { ServerModel } from '../server/server' | ||
32 | |||
33 | enum ScopeNames { | ||
34 | WITH_ALL = 'WITH_ALL' | ||
35 | } | ||
36 | |||
37 | function buildActorWithAvatarInclude () { | ||
38 | return { | ||
39 | attributes: [ 'preferredUsername' ], | ||
40 | model: () => ActorModel.unscoped(), | ||
41 | required: true, | ||
42 | include: [ | ||
43 | { | ||
44 | attributes: [ 'filename' ], | ||
45 | model: () => AvatarModel.unscoped(), | ||
46 | required: false | ||
47 | }, | ||
48 | { | ||
49 | attributes: [ 'host' ], | ||
50 | model: () => ServerModel.unscoped(), | ||
51 | required: false | ||
52 | } | ||
53 | ] | ||
54 | } | ||
55 | } | ||
56 | |||
57 | function buildVideoInclude (required: boolean) { | ||
58 | return { | ||
59 | attributes: [ 'id', 'uuid', 'name' ], | ||
60 | model: () => VideoModel.unscoped(), | ||
61 | required | ||
62 | } | ||
63 | } | ||
64 | |||
65 | function buildChannelInclude (required: boolean, withActor = false) { | ||
66 | return { | ||
67 | required, | ||
68 | attributes: [ 'id', 'name' ], | ||
69 | model: () => VideoChannelModel.unscoped(), | ||
70 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
71 | } | ||
72 | } | ||
73 | |||
74 | function buildAccountInclude (required: boolean, withActor = false) { | ||
75 | return { | ||
76 | required, | ||
77 | attributes: [ 'id', 'name' ], | ||
78 | model: () => AccountModel.unscoped(), | ||
79 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
80 | } | ||
81 | } | ||
82 | |||
83 | @Scopes({ | ||
84 | [ScopeNames.WITH_ALL]: { | ||
85 | include: [ | ||
86 | Object.assign(buildVideoInclude(false), { | ||
87 | include: [ buildChannelInclude(true, true) ] | ||
88 | }), | ||
89 | |||
90 | { | ||
91 | attributes: [ 'id', 'originCommentId' ], | ||
92 | model: () => VideoCommentModel.unscoped(), | ||
93 | required: false, | ||
94 | include: [ | ||
95 | buildAccountInclude(true, true), | ||
96 | buildVideoInclude(true) | ||
97 | ] | ||
98 | }, | ||
99 | |||
100 | { | ||
101 | attributes: [ 'id' ], | ||
102 | model: () => VideoAbuseModel.unscoped(), | ||
103 | required: false, | ||
104 | include: [ buildVideoInclude(true) ] | ||
105 | }, | ||
106 | |||
107 | { | ||
108 | attributes: [ 'id' ], | ||
109 | model: () => VideoBlacklistModel.unscoped(), | ||
110 | required: false, | ||
111 | include: [ buildVideoInclude(true) ] | ||
112 | }, | ||
113 | |||
114 | { | ||
115 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | ||
116 | model: () => VideoImportModel.unscoped(), | ||
117 | required: false, | ||
118 | include: [ buildVideoInclude(false) ] | ||
119 | }, | ||
120 | |||
121 | { | ||
122 | attributes: [ 'id' ], | ||
123 | model: () => ActorFollowModel.unscoped(), | ||
124 | required: false, | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [ 'preferredUsername' ], | ||
128 | model: () => ActorModel.unscoped(), | ||
129 | required: true, | ||
130 | as: 'ActorFollower', | ||
131 | include: [ | ||
132 | { | ||
133 | attributes: [ 'id', 'name' ], | ||
134 | model: () => AccountModel.unscoped(), | ||
135 | required: true | ||
136 | }, | ||
137 | { | ||
138 | attributes: [ 'filename' ], | ||
139 | model: () => AvatarModel.unscoped(), | ||
140 | required: false | ||
141 | }, | ||
142 | { | ||
143 | attributes: [ 'host' ], | ||
144 | model: () => ServerModel.unscoped(), | ||
145 | required: false | ||
146 | } | ||
147 | ] | ||
148 | }, | ||
149 | { | ||
150 | attributes: [ 'preferredUsername' ], | ||
151 | model: () => ActorModel.unscoped(), | ||
152 | required: true, | ||
153 | as: 'ActorFollowing', | ||
154 | include: [ | ||
155 | buildChannelInclude(false), | ||
156 | buildAccountInclude(false) | ||
157 | ] | ||
158 | } | ||
159 | ] | ||
160 | }, | ||
161 | |||
162 | buildAccountInclude(false, true) | ||
163 | ] | ||
164 | } | ||
165 | }) | ||
166 | @Table({ | ||
167 | tableName: 'userNotification', | ||
168 | indexes: [ | ||
169 | { | ||
170 | fields: [ 'userId' ] | ||
171 | }, | ||
172 | { | ||
173 | fields: [ 'videoId' ], | ||
174 | where: { | ||
175 | videoId: { | ||
176 | [Op.ne]: null | ||
177 | } | ||
178 | } | ||
179 | }, | ||
180 | { | ||
181 | fields: [ 'commentId' ], | ||
182 | where: { | ||
183 | commentId: { | ||
184 | [Op.ne]: null | ||
185 | } | ||
186 | } | ||
187 | }, | ||
188 | { | ||
189 | fields: [ 'videoAbuseId' ], | ||
190 | where: { | ||
191 | videoAbuseId: { | ||
192 | [Op.ne]: null | ||
193 | } | ||
194 | } | ||
195 | }, | ||
196 | { | ||
197 | fields: [ 'videoBlacklistId' ], | ||
198 | where: { | ||
199 | videoBlacklistId: { | ||
200 | [Op.ne]: null | ||
201 | } | ||
202 | } | ||
203 | }, | ||
204 | { | ||
205 | fields: [ 'videoImportId' ], | ||
206 | where: { | ||
207 | videoImportId: { | ||
208 | [Op.ne]: null | ||
209 | } | ||
210 | } | ||
211 | }, | ||
212 | { | ||
213 | fields: [ 'accountId' ], | ||
214 | where: { | ||
215 | accountId: { | ||
216 | [Op.ne]: null | ||
217 | } | ||
218 | } | ||
219 | }, | ||
220 | { | ||
221 | fields: [ 'actorFollowId' ], | ||
222 | where: { | ||
223 | actorFollowId: { | ||
224 | [Op.ne]: null | ||
225 | } | ||
226 | } | ||
227 | } | ||
228 | ] | ||
229 | }) | ||
230 | export class UserNotificationModel extends Model<UserNotificationModel> { | ||
231 | |||
232 | @AllowNull(false) | ||
233 | @Default(null) | ||
234 | @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) | ||
235 | @Column | ||
236 | type: UserNotificationType | ||
237 | |||
238 | @AllowNull(false) | ||
239 | @Default(false) | ||
240 | @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) | ||
241 | @Column | ||
242 | read: boolean | ||
243 | |||
244 | @CreatedAt | ||
245 | createdAt: Date | ||
246 | |||
247 | @UpdatedAt | ||
248 | updatedAt: Date | ||
249 | |||
250 | @ForeignKey(() => UserModel) | ||
251 | @Column | ||
252 | userId: number | ||
253 | |||
254 | @BelongsTo(() => UserModel, { | ||
255 | foreignKey: { | ||
256 | allowNull: false | ||
257 | }, | ||
258 | onDelete: 'cascade' | ||
259 | }) | ||
260 | User: UserModel | ||
261 | |||
262 | @ForeignKey(() => VideoModel) | ||
263 | @Column | ||
264 | videoId: number | ||
265 | |||
266 | @BelongsTo(() => VideoModel, { | ||
267 | foreignKey: { | ||
268 | allowNull: true | ||
269 | }, | ||
270 | onDelete: 'cascade' | ||
271 | }) | ||
272 | Video: VideoModel | ||
273 | |||
274 | @ForeignKey(() => VideoCommentModel) | ||
275 | @Column | ||
276 | commentId: number | ||
277 | |||
278 | @BelongsTo(() => VideoCommentModel, { | ||
279 | foreignKey: { | ||
280 | allowNull: true | ||
281 | }, | ||
282 | onDelete: 'cascade' | ||
283 | }) | ||
284 | Comment: VideoCommentModel | ||
285 | |||
286 | @ForeignKey(() => VideoAbuseModel) | ||
287 | @Column | ||
288 | videoAbuseId: number | ||
289 | |||
290 | @BelongsTo(() => VideoAbuseModel, { | ||
291 | foreignKey: { | ||
292 | allowNull: true | ||
293 | }, | ||
294 | onDelete: 'cascade' | ||
295 | }) | ||
296 | VideoAbuse: VideoAbuseModel | ||
297 | |||
298 | @ForeignKey(() => VideoBlacklistModel) | ||
299 | @Column | ||
300 | videoBlacklistId: number | ||
301 | |||
302 | @BelongsTo(() => VideoBlacklistModel, { | ||
303 | foreignKey: { | ||
304 | allowNull: true | ||
305 | }, | ||
306 | onDelete: 'cascade' | ||
307 | }) | ||
308 | VideoBlacklist: VideoBlacklistModel | ||
309 | |||
310 | @ForeignKey(() => VideoImportModel) | ||
311 | @Column | ||
312 | videoImportId: number | ||
313 | |||
314 | @BelongsTo(() => VideoImportModel, { | ||
315 | foreignKey: { | ||
316 | allowNull: true | ||
317 | }, | ||
318 | onDelete: 'cascade' | ||
319 | }) | ||
320 | VideoImport: VideoImportModel | ||
321 | |||
322 | @ForeignKey(() => AccountModel) | ||
323 | @Column | ||
324 | accountId: number | ||
325 | |||
326 | @BelongsTo(() => AccountModel, { | ||
327 | foreignKey: { | ||
328 | allowNull: true | ||
329 | }, | ||
330 | onDelete: 'cascade' | ||
331 | }) | ||
332 | Account: AccountModel | ||
333 | |||
334 | @ForeignKey(() => ActorFollowModel) | ||
335 | @Column | ||
336 | actorFollowId: number | ||
337 | |||
338 | @BelongsTo(() => ActorFollowModel, { | ||
339 | foreignKey: { | ||
340 | allowNull: true | ||
341 | }, | ||
342 | onDelete: 'cascade' | ||
343 | }) | ||
344 | ActorFollow: ActorFollowModel | ||
345 | |||
346 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | ||
347 | const query: IFindOptions<UserNotificationModel> = { | ||
348 | offset: start, | ||
349 | limit: count, | ||
350 | order: getSort(sort), | ||
351 | where: { | ||
352 | userId | ||
353 | } | ||
354 | } | ||
355 | |||
356 | if (unread !== undefined) query.where['read'] = !unread | ||
357 | |||
358 | return UserNotificationModel.scope(ScopeNames.WITH_ALL) | ||
359 | .findAndCountAll(query) | ||
360 | .then(({ rows, count }) => { | ||
361 | return { | ||
362 | data: rows, | ||
363 | total: count | ||
364 | } | ||
365 | }) | ||
366 | } | ||
367 | |||
368 | static markAsRead (userId: number, notificationIds: number[]) { | ||
369 | const query = { | ||
370 | where: { | ||
371 | userId, | ||
372 | id: { | ||
373 | [Op.any]: notificationIds | ||
374 | } | ||
375 | } | ||
376 | } | ||
377 | |||
378 | return UserNotificationModel.update({ read: true }, query) | ||
379 | } | ||
380 | |||
381 | static markAllAsRead (userId: number) { | ||
382 | const query = { where: { userId } } | ||
383 | |||
384 | return UserNotificationModel.update({ read: true }, query) | ||
385 | } | ||
386 | |||
387 | toFormattedJSON (): UserNotification { | ||
388 | const video = this.Video | ||
389 | ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) | ||
390 | : undefined | ||
391 | |||
392 | const videoImport = this.VideoImport ? { | ||
393 | id: this.VideoImport.id, | ||
394 | video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, | ||
395 | torrentName: this.VideoImport.torrentName, | ||
396 | magnetUri: this.VideoImport.magnetUri, | ||
397 | targetUrl: this.VideoImport.targetUrl | ||
398 | } : undefined | ||
399 | |||
400 | const comment = this.Comment ? { | ||
401 | id: this.Comment.id, | ||
402 | threadId: this.Comment.getThreadId(), | ||
403 | account: this.formatActor(this.Comment.Account), | ||
404 | video: this.formatVideo(this.Comment.Video) | ||
405 | } : undefined | ||
406 | |||
407 | const videoAbuse = this.VideoAbuse ? { | ||
408 | id: this.VideoAbuse.id, | ||
409 | video: this.formatVideo(this.VideoAbuse.Video) | ||
410 | } : undefined | ||
411 | |||
412 | const videoBlacklist = this.VideoBlacklist ? { | ||
413 | id: this.VideoBlacklist.id, | ||
414 | video: this.formatVideo(this.VideoBlacklist.Video) | ||
415 | } : undefined | ||
416 | |||
417 | const account = this.Account ? this.formatActor(this.Account) : undefined | ||
418 | |||
419 | const actorFollow = this.ActorFollow ? { | ||
420 | id: this.ActorFollow.id, | ||
421 | follower: { | ||
422 | id: this.ActorFollow.ActorFollower.Account.id, | ||
423 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | ||
424 | name: this.ActorFollow.ActorFollower.preferredUsername, | ||
425 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined, | ||
426 | host: this.ActorFollow.ActorFollower.getHost() | ||
427 | }, | ||
428 | following: { | ||
429 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', | ||
430 | displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), | ||
431 | name: this.ActorFollow.ActorFollowing.preferredUsername | ||
432 | } | ||
433 | } : undefined | ||
434 | |||
435 | return { | ||
436 | id: this.id, | ||
437 | type: this.type, | ||
438 | read: this.read, | ||
439 | video, | ||
440 | videoImport, | ||
441 | comment, | ||
442 | videoAbuse, | ||
443 | videoBlacklist, | ||
444 | account, | ||
445 | actorFollow, | ||
446 | createdAt: this.createdAt.toISOString(), | ||
447 | updatedAt: this.updatedAt.toISOString() | ||
448 | } | ||
449 | } | ||
450 | |||
451 | private formatVideo (video: VideoModel) { | ||
452 | return { | ||
453 | id: video.id, | ||
454 | uuid: video.uuid, | ||
455 | name: video.name | ||
456 | } | ||
457 | } | ||
458 | |||
459 | private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { | ||
460 | const avatar = accountOrChannel.Actor.Avatar | ||
461 | ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() } | ||
462 | : undefined | ||
463 | |||
464 | return { | ||
465 | id: accountOrChannel.id, | ||
466 | displayName: accountOrChannel.getDisplayName(), | ||
467 | name: accountOrChannel.Actor.preferredUsername, | ||
468 | host: accountOrChannel.Actor.getHost(), | ||
469 | avatar | ||
470 | } | ||
471 | } | ||
472 | } | ||
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts index 0476cad9d..15cb399c9 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/account/user-video-history.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { VideoModel } from '../video/video' | 2 | import { VideoModel } from '../video/video' |
3 | import { UserModel } from './user' | 3 | import { UserModel } from './user' |
4 | import { Transaction, Op, DestroyOptions } from 'sequelize' | ||
4 | 5 | ||
5 | @Table({ | 6 | @Table({ |
6 | tableName: 'userVideoHistory', | 7 | tableName: 'userVideoHistory', |
@@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> { | |||
52 | onDelete: 'CASCADE' | 53 | onDelete: 'CASCADE' |
53 | }) | 54 | }) |
54 | User: UserModel | 55 | User: UserModel |
56 | |||
57 | static listForApi (user: UserModel, start: number, count: number) { | ||
58 | return VideoModel.listForApi({ | ||
59 | start, | ||
60 | count, | ||
61 | sort: '-UserVideoHistories.updatedAt', | ||
62 | nsfw: null, // All | ||
63 | includeLocalVideos: true, | ||
64 | withFiles: false, | ||
65 | user, | ||
66 | historyOfUser: user | ||
67 | }) | ||
68 | } | ||
69 | |||
70 | static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) { | ||
71 | const query: DestroyOptions = { | ||
72 | where: { | ||
73 | userId: user.id | ||
74 | }, | ||
75 | transaction: t | ||
76 | } | ||
77 | |||
78 | if (beforeDate) { | ||
79 | query.where.updatedAt = { | ||
80 | [Op.lt]: beforeDate | ||
81 | } | ||
82 | } | ||
83 | |||
84 | return UserVideoHistoryModel.destroy(query) | ||
85 | } | ||
55 | } | 86 | } |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 1843603f1..017a96657 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -32,6 +32,7 @@ import { | |||
32 | isUserUsernameValid, | 32 | isUserUsernameValid, |
33 | isUserVideoQuotaDailyValid, | 33 | isUserVideoQuotaDailyValid, |
34 | isUserVideoQuotaValid, | 34 | isUserVideoQuotaValid, |
35 | isUserVideosHistoryEnabledValid, | ||
35 | isUserWebTorrentEnabledValid | 36 | isUserWebTorrentEnabledValid |
36 | } from '../../helpers/custom-validators/users' | 37 | } from '../../helpers/custom-validators/users' |
37 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 38 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
@@ -43,6 +44,11 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | |||
43 | import { values } from 'lodash' | 44 | import { values } from 'lodash' |
44 | import { NSFW_POLICY_TYPES } from '../../initializers' | 45 | import { NSFW_POLICY_TYPES } from '../../initializers' |
45 | import { clearCacheByUserId } from '../../lib/oauth-model' | 46 | import { clearCacheByUserId } from '../../lib/oauth-model' |
47 | import { UserNotificationSettingModel } from './user-notification-setting' | ||
48 | import { VideoModel } from '../video/video' | ||
49 | import { ActorModel } from '../activitypub/actor' | ||
50 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
51 | import { VideoImportModel } from '../video/video-import' | ||
46 | 52 | ||
47 | enum ScopeNames { | 53 | enum ScopeNames { |
48 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 54 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -53,6 +59,10 @@ enum ScopeNames { | |||
53 | { | 59 | { |
54 | model: () => AccountModel, | 60 | model: () => AccountModel, |
55 | required: true | 61 | required: true |
62 | }, | ||
63 | { | ||
64 | model: () => UserNotificationSettingModel, | ||
65 | required: true | ||
56 | } | 66 | } |
57 | ] | 67 | ] |
58 | }) | 68 | }) |
@@ -63,6 +73,10 @@ enum ScopeNames { | |||
63 | model: () => AccountModel, | 73 | model: () => AccountModel, |
64 | required: true, | 74 | required: true, |
65 | include: [ () => VideoChannelModel ] | 75 | include: [ () => VideoChannelModel ] |
76 | }, | ||
77 | { | ||
78 | model: () => UserNotificationSettingModel, | ||
79 | required: true | ||
66 | } | 80 | } |
67 | ] | 81 | ] |
68 | } | 82 | } |
@@ -116,6 +130,12 @@ export class UserModel extends Model<UserModel> { | |||
116 | 130 | ||
117 | @AllowNull(false) | 131 | @AllowNull(false) |
118 | @Default(true) | 132 | @Default(true) |
133 | @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) | ||
134 | @Column | ||
135 | videosHistoryEnabled: boolean | ||
136 | |||
137 | @AllowNull(false) | ||
138 | @Default(true) | ||
119 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) | 139 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) |
120 | @Column | 140 | @Column |
121 | autoPlayVideo: boolean | 141 | autoPlayVideo: boolean |
@@ -160,6 +180,19 @@ export class UserModel extends Model<UserModel> { | |||
160 | }) | 180 | }) |
161 | Account: AccountModel | 181 | Account: AccountModel |
162 | 182 | ||
183 | @HasOne(() => UserNotificationSettingModel, { | ||
184 | foreignKey: 'userId', | ||
185 | onDelete: 'cascade', | ||
186 | hooks: true | ||
187 | }) | ||
188 | NotificationSetting: UserNotificationSettingModel | ||
189 | |||
190 | @HasMany(() => VideoImportModel, { | ||
191 | foreignKey: 'userId', | ||
192 | onDelete: 'cascade' | ||
193 | }) | ||
194 | VideoImports: VideoImportModel[] | ||
195 | |||
163 | @HasMany(() => OAuthTokenModel, { | 196 | @HasMany(() => OAuthTokenModel, { |
164 | foreignKey: 'userId', | 197 | foreignKey: 'userId', |
165 | onDelete: 'cascade' | 198 | onDelete: 'cascade' |
@@ -242,13 +275,12 @@ export class UserModel extends Model<UserModel> { | |||
242 | }) | 275 | }) |
243 | } | 276 | } |
244 | 277 | ||
245 | static listEmailsWithRight (right: UserRight) { | 278 | static listWithRight (right: UserRight) { |
246 | const roles = Object.keys(USER_ROLE_LABELS) | 279 | const roles = Object.keys(USER_ROLE_LABELS) |
247 | .map(k => parseInt(k, 10) as UserRole) | 280 | .map(k => parseInt(k, 10) as UserRole) |
248 | .filter(role => hasUserRight(role, right)) | 281 | .filter(role => hasUserRight(role, right)) |
249 | 282 | ||
250 | const query = { | 283 | const query = { |
251 | attribute: [ 'email' ], | ||
252 | where: { | 284 | where: { |
253 | role: { | 285 | role: { |
254 | [Sequelize.Op.in]: roles | 286 | [Sequelize.Op.in]: roles |
@@ -256,9 +288,56 @@ export class UserModel extends Model<UserModel> { | |||
256 | } | 288 | } |
257 | } | 289 | } |
258 | 290 | ||
259 | return UserModel.unscoped() | 291 | return UserModel.findAll(query) |
260 | .findAll(query) | 292 | } |
261 | .then(u => u.map(u => u.email)) | 293 | |
294 | static listUserSubscribersOf (actorId: number) { | ||
295 | const query = { | ||
296 | include: [ | ||
297 | { | ||
298 | model: UserNotificationSettingModel.unscoped(), | ||
299 | required: true | ||
300 | }, | ||
301 | { | ||
302 | attributes: [ 'userId' ], | ||
303 | model: AccountModel.unscoped(), | ||
304 | required: true, | ||
305 | include: [ | ||
306 | { | ||
307 | attributes: [ ], | ||
308 | model: ActorModel.unscoped(), | ||
309 | required: true, | ||
310 | where: { | ||
311 | serverId: null | ||
312 | }, | ||
313 | include: [ | ||
314 | { | ||
315 | attributes: [ ], | ||
316 | as: 'ActorFollowings', | ||
317 | model: ActorFollowModel.unscoped(), | ||
318 | required: true, | ||
319 | where: { | ||
320 | targetActorId: actorId | ||
321 | } | ||
322 | } | ||
323 | ] | ||
324 | } | ||
325 | ] | ||
326 | } | ||
327 | ] | ||
328 | } | ||
329 | |||
330 | return UserModel.unscoped().findAll(query) | ||
331 | } | ||
332 | |||
333 | static listByUsernames (usernames: string[]) { | ||
334 | const query = { | ||
335 | where: { | ||
336 | username: usernames | ||
337 | } | ||
338 | } | ||
339 | |||
340 | return UserModel.findAll(query) | ||
262 | } | 341 | } |
263 | 342 | ||
264 | static loadById (id: number) { | 343 | static loadById (id: number) { |
@@ -307,6 +386,95 @@ export class UserModel extends Model<UserModel> { | |||
307 | return UserModel.findOne(query) | 386 | return UserModel.findOne(query) |
308 | } | 387 | } |
309 | 388 | ||
389 | static loadByVideoId (videoId: number) { | ||
390 | const query = { | ||
391 | include: [ | ||
392 | { | ||
393 | required: true, | ||
394 | attributes: [ 'id' ], | ||
395 | model: AccountModel.unscoped(), | ||
396 | include: [ | ||
397 | { | ||
398 | required: true, | ||
399 | attributes: [ 'id' ], | ||
400 | model: VideoChannelModel.unscoped(), | ||
401 | include: [ | ||
402 | { | ||
403 | required: true, | ||
404 | attributes: [ 'id' ], | ||
405 | model: VideoModel.unscoped(), | ||
406 | where: { | ||
407 | id: videoId | ||
408 | } | ||
409 | } | ||
410 | ] | ||
411 | } | ||
412 | ] | ||
413 | } | ||
414 | ] | ||
415 | } | ||
416 | |||
417 | return UserModel.findOne(query) | ||
418 | } | ||
419 | |||
420 | static loadByVideoImportId (videoImportId: number) { | ||
421 | const query = { | ||
422 | include: [ | ||
423 | { | ||
424 | required: true, | ||
425 | attributes: [ 'id' ], | ||
426 | model: VideoImportModel.unscoped(), | ||
427 | where: { | ||
428 | id: videoImportId | ||
429 | } | ||
430 | } | ||
431 | ] | ||
432 | } | ||
433 | |||
434 | return UserModel.findOne(query) | ||
435 | } | ||
436 | |||
437 | static loadByChannelActorId (videoChannelActorId: number) { | ||
438 | const query = { | ||
439 | include: [ | ||
440 | { | ||
441 | required: true, | ||
442 | attributes: [ 'id' ], | ||
443 | model: AccountModel.unscoped(), | ||
444 | include: [ | ||
445 | { | ||
446 | required: true, | ||
447 | attributes: [ 'id' ], | ||
448 | model: VideoChannelModel.unscoped(), | ||
449 | where: { | ||
450 | actorId: videoChannelActorId | ||
451 | } | ||
452 | } | ||
453 | ] | ||
454 | } | ||
455 | ] | ||
456 | } | ||
457 | |||
458 | return UserModel.findOne(query) | ||
459 | } | ||
460 | |||
461 | static loadByAccountActorId (accountActorId: number) { | ||
462 | const query = { | ||
463 | include: [ | ||
464 | { | ||
465 | required: true, | ||
466 | attributes: [ 'id' ], | ||
467 | model: AccountModel.unscoped(), | ||
468 | where: { | ||
469 | actorId: accountActorId | ||
470 | } | ||
471 | } | ||
472 | ] | ||
473 | } | ||
474 | |||
475 | return UserModel.findOne(query) | ||
476 | } | ||
477 | |||
310 | static getOriginalVideoFileTotalFromUser (user: UserModel) { | 478 | static getOriginalVideoFileTotalFromUser (user: UserModel) { |
311 | // Don't use sequelize because we need to use a sub query | 479 | // Don't use sequelize because we need to use a sub query |
312 | const query = UserModel.generateUserQuotaBaseSQL() | 480 | const query = UserModel.generateUserQuotaBaseSQL() |
@@ -363,6 +531,7 @@ export class UserModel extends Model<UserModel> { | |||
363 | emailVerified: this.emailVerified, | 531 | emailVerified: this.emailVerified, |
364 | nsfwPolicy: this.nsfwPolicy, | 532 | nsfwPolicy: this.nsfwPolicy, |
365 | webTorrentEnabled: this.webTorrentEnabled, | 533 | webTorrentEnabled: this.webTorrentEnabled, |
534 | videosHistoryEnabled: this.videosHistoryEnabled, | ||
366 | autoPlayVideo: this.autoPlayVideo, | 535 | autoPlayVideo: this.autoPlayVideo, |
367 | role: this.role, | 536 | role: this.role, |
368 | roleLabel: USER_ROLE_LABELS[ this.role ], | 537 | roleLabel: USER_ROLE_LABELS[ this.role ], |
@@ -372,6 +541,7 @@ export class UserModel extends Model<UserModel> { | |||
372 | blocked: this.blocked, | 541 | blocked: this.blocked, |
373 | blockedReason: this.blockedReason, | 542 | blockedReason: this.blockedReason, |
374 | account: this.Account.toFormattedJSON(), | 543 | account: this.Account.toFormattedJSON(), |
544 | notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, | ||
375 | videoChannels: [], | 545 | videoChannels: [], |
376 | videoQuotaUsed: videoQuotaUsed !== undefined | 546 | videoQuotaUsed: videoQuotaUsed !== undefined |
377 | ? parseInt(videoQuotaUsed, 10) | 547 | ? parseInt(videoQuotaUsed, 10) |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 0a6935083..796e07a42 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -127,22 +127,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
127 | if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) | 127 | if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) |
128 | } | 128 | } |
129 | 129 | ||
130 | static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) { | ||
131 | if (goodInboxes.length === 0 && badInboxes.length === 0) return | ||
132 | |||
133 | logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length) | ||
134 | |||
135 | if (goodInboxes.length !== 0) { | ||
136 | ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t) | ||
137 | .catch(err => logger.error('Cannot increment scores of good actor follows.', { err })) | ||
138 | } | ||
139 | |||
140 | if (badInboxes.length !== 0) { | ||
141 | ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t) | ||
142 | .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err })) | ||
143 | } | ||
144 | } | ||
145 | |||
146 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { | 130 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { |
147 | const query = { | 131 | const query = { |
148 | where: { | 132 | where: { |
@@ -323,7 +307,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
323 | }) | 307 | }) |
324 | } | 308 | } |
325 | 309 | ||
326 | static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { | 310 | static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { |
327 | const query = { | 311 | const query = { |
328 | distinct: true, | 312 | distinct: true, |
329 | offset: start, | 313 | offset: start, |
@@ -351,7 +335,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
351 | as: 'ActorFollowing', | 335 | as: 'ActorFollowing', |
352 | required: true, | 336 | required: true, |
353 | where: { | 337 | where: { |
354 | id | 338 | id: actorId |
355 | } | 339 | } |
356 | } | 340 | } |
357 | ] | 341 | ] |
@@ -366,7 +350,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
366 | }) | 350 | }) |
367 | } | 351 | } |
368 | 352 | ||
369 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | 353 | static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { |
370 | const query = { | 354 | const query = { |
371 | attributes: [], | 355 | attributes: [], |
372 | distinct: true, | 356 | distinct: true, |
@@ -374,7 +358,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
374 | limit: count, | 358 | limit: count, |
375 | order: getSort(sort), | 359 | order: getSort(sort), |
376 | where: { | 360 | where: { |
377 | actorId: id | 361 | actorId: actorId |
378 | }, | 362 | }, |
379 | include: [ | 363 | include: [ |
380 | { | 364 | { |
@@ -464,6 +448,22 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
464 | } | 448 | } |
465 | } | 449 | } |
466 | 450 | ||
451 | static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { | ||
452 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
453 | 'WHERE id IN (' + | ||
454 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
455 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
456 | `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + | ||
457 | ')' | ||
458 | |||
459 | const options = { | ||
460 | type: Sequelize.QueryTypes.BULKUPDATE, | ||
461 | transaction: t | ||
462 | } | ||
463 | |||
464 | return ActorFollowModel.sequelize.query(query, options) | ||
465 | } | ||
466 | |||
467 | private static async createListAcceptedFollowForApiQuery ( | 467 | private static async createListAcceptedFollowForApiQuery ( |
468 | type: 'followers' | 'following', | 468 | type: 'followers' | 'following', |
469 | actorIds: number[], | 469 | actorIds: number[], |
@@ -518,24 +518,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
518 | } | 518 | } |
519 | } | 519 | } |
520 | 520 | ||
521 | private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) { | ||
522 | const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',') | ||
523 | |||
524 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | ||
525 | 'WHERE id IN (' + | ||
526 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | ||
527 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | ||
528 | 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' + | ||
529 | ')' | ||
530 | |||
531 | const options = t ? { | ||
532 | type: Sequelize.QueryTypes.BULKUPDATE, | ||
533 | transaction: t | ||
534 | } : undefined | ||
535 | |||
536 | return ActorFollowModel.sequelize.query(query, options) | ||
537 | } | ||
538 | |||
539 | private static listBadActorFollows () { | 521 | private static listBadActorFollows () { |
540 | const query = { | 522 | const query = { |
541 | where: { | 523 | where: { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 12b83916e..dda57a8ba 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -219,6 +219,7 @@ export class ActorModel extends Model<ActorModel> { | |||
219 | name: 'actorId', | 219 | name: 'actorId', |
220 | allowNull: false | 220 | allowNull: false |
221 | }, | 221 | }, |
222 | as: 'ActorFollowings', | ||
222 | onDelete: 'cascade' | 223 | onDelete: 'cascade' |
223 | }) | 224 | }) |
224 | ActorFollowing: ActorFollowModel[] | 225 | ActorFollowing: ActorFollowModel[] |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 9de4356b4..8f2ef2d9a 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, throwIfNotValid } from '../utils' | 16 | import { getVideoSort, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' | 18 | import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 125 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 126 | ||
127 | videoFile.Video.removeFile(videoFile) | 127 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
129 | 129 | ||
130 | return undefined | 130 | return undefined |
@@ -395,7 +395,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
395 | ] | 395 | ] |
396 | } | 396 | } |
397 | 397 | ||
398 | return VideoRedundancyModel.find(query as any) // FIXME: typings | 398 | return VideoRedundancyModel.findOne(query as any) // FIXME: typings |
399 | .then((r: any) => ({ | 399 | .then((r: any) => ({ |
400 | totalUsed: parseInt(r.totalUsed.toString(), 10), | 400 | totalUsed: parseInt(r.totalUsed.toString(), 10), |
401 | totalVideos: r.totalVideos, | 401 | totalVideos: r.totalVideos, |
@@ -415,8 +415,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
415 | expires: this.expiresOn.toISOString(), | 415 | expires: this.expiresOn.toISOString(), |
416 | url: { | 416 | url: { |
417 | type: 'Link', | 417 | type: 'Link', |
418 | mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 418 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
419 | mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 419 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
420 | href: this.fileUrl, | 420 | href: this.fileUrl, |
421 | height: this.VideoFile.resolution, | 421 | height: this.VideoFile.resolution, |
422 | size: this.VideoFile.size, | 422 | size: this.VideoFile.size, |
diff --git a/server/models/utils.ts b/server/models/utils.ts index 60b0906e8..5b4093aec 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -29,7 +29,11 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | |||
29 | ] | 29 | ] |
30 | } | 30 | } |
31 | 31 | ||
32 | return [ [ field, direction ], lastSort ] | 32 | const firstSort = typeof field === 'string' ? |
33 | field.split('.').concat([ direction ]) : | ||
34 | [ field, direction ] | ||
35 | |||
36 | return [ firstSort, lastSort ] | ||
33 | } | 37 | } |
34 | 38 | ||
35 | function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | 39 | function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,17 +1,4 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
16 | import { VideoAbuse } from '../../../shared/models/videos' | 3 | import { VideoAbuse } from '../../../shared/models/videos' |
17 | import { | 4 | import { |
@@ -19,7 +6,6 @@ import { | |||
19 | isVideoAbuseReasonValid, | 6 | isVideoAbuseReasonValid, |
20 | isVideoAbuseStateValid | 7 | isVideoAbuseStateValid |
21 | } from '../../helpers/custom-validators/video-abuses' | 8 | } from '../../helpers/custom-validators/video-abuses' |
22 | import { Emailer } from '../../lib/emailer' | ||
23 | import { AccountModel } from '../account/account' | 9 | import { AccountModel } from '../account/account' |
24 | import { getSort, throwIfNotValid } from '../utils' | 10 | import { getSort, throwIfNotValid } from '../utils' |
25 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' | |||
40 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | 26 | export class VideoAbuseModel extends Model<VideoAbuseModel> { |
41 | 27 | ||
42 | @AllowNull(false) | 28 | @AllowNull(false) |
29 | @Default(null) | ||
43 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | 30 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) |
44 | @Column | 31 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) |
45 | reason: string | 32 | reason: string |
46 | 33 | ||
47 | @AllowNull(false) | 34 | @AllowNull(false) |
@@ -86,11 +73,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
86 | }) | 73 | }) |
87 | Video: VideoModel | 74 | Video: VideoModel |
88 | 75 | ||
89 | @AfterCreate | ||
90 | static sendEmailNotification (instance: VideoAbuseModel) { | ||
91 | return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) | ||
92 | } | ||
93 | |||
94 | static loadByIdAndVideoId (id: number, videoId: number) { | 76 | static loadByIdAndVideoId (id: number, videoId: number) { |
95 | const query = { | 77 | const query = { |
96 | where: { | 78 | where: { |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..3b567e488 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,21 +1,7 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AfterDestroy, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from './video' | 3 | import { VideoModel } from './video' |
17 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 4 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' |
18 | import { Emailer } from '../../lib/emailer' | ||
19 | import { VideoBlacklist } from '../../../shared/models/videos' | 5 | import { VideoBlacklist } from '../../../shared/models/videos' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
21 | 7 | ||
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
35 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) | 21 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) |
36 | reason: string | 22 | reason: string |
37 | 23 | ||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | unfederated: boolean | ||
27 | |||
38 | @CreatedAt | 28 | @CreatedAt |
39 | createdAt: Date | 29 | createdAt: Date |
40 | 30 | ||
@@ -53,16 +43,6 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
53 | }) | 43 | }) |
54 | Video: VideoModel | 44 | Video: VideoModel |
55 | 45 | ||
56 | @AfterCreate | ||
57 | static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { | ||
58 | return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) | ||
59 | } | ||
60 | |||
61 | @AfterDestroy | ||
62 | static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { | ||
63 | return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) | ||
64 | } | ||
65 | |||
66 | static listForApi (start: number, count: number, sort: SortType) { | 46 | static listForApi (start: number, count: number, sort: SortType) { |
67 | const query = { | 47 | const query = { |
68 | offset: start, | 48 | offset: start, |
@@ -103,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
103 | createdAt: this.createdAt, | 83 | createdAt: this.createdAt, |
104 | updatedAt: this.updatedAt, | 84 | updatedAt: this.updatedAt, |
105 | reason: this.reason, | 85 | reason: this.reason, |
86 | unfederated: this.unfederated, | ||
106 | 87 | ||
107 | video: { | 88 | video: { |
108 | id: video.id, | 89 | id: video.id, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f4586917e..5598d80f6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
233 | }) | 233 | }) |
234 | } | 234 | } |
235 | 235 | ||
236 | static listLocalsForSitemap (sort: string) { | ||
237 | const query = { | ||
238 | attributes: [ ], | ||
239 | offset: 0, | ||
240 | order: getSort(sort), | ||
241 | include: [ | ||
242 | { | ||
243 | attributes: [ 'preferredUsername', 'serverId' ], | ||
244 | model: ActorModel.unscoped(), | ||
245 | where: { | ||
246 | serverId: null | ||
247 | } | ||
248 | } | ||
249 | ] | ||
250 | } | ||
251 | |||
252 | return VideoChannelModel | ||
253 | .unscoped() | ||
254 | .findAll(query) | ||
255 | } | ||
256 | |||
236 | static searchForApi (options: { | 257 | static searchForApi (options: { |
237 | actorId: number | 258 | actorId: number |
238 | search: string | 259 | search: string |
@@ -449,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
449 | getDisplayName () { | 470 | getDisplayName () { |
450 | return this.name | 471 | return this.name |
451 | } | 472 | } |
473 | |||
474 | isOutdated () { | ||
475 | return this.Actor.isOutdated() | ||
476 | } | ||
452 | } | 477 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index dd6d08139..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co | |||
18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
21 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | 22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' |
23 | import { AccountModel } from '../account/account' | 23 | import { AccountModel } from '../account/account' |
24 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
@@ -29,6 +29,9 @@ import { VideoModel } from './video' | |||
29 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | 30 | import { getServerActor } from '../../helpers/utils' |
31 | import { UserModel } from '../account/user' | 31 | import { UserModel } from '../account/user' |
32 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | ||
33 | import { regexpCapture } from '../../helpers/regexp' | ||
34 | import { uniq } from 'lodash' | ||
32 | 35 | ||
33 | enum ScopeNames { | 36 | enum ScopeNames { |
34 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 37 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
370 | id: { | 373 | id: { |
371 | [ Sequelize.Op.in ]: Sequelize.literal('(' + | 374 | [ Sequelize.Op.in ]: Sequelize.literal('(' + |
372 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 375 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
373 | 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + | 376 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
374 | 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + | 377 | 'UNION ' + |
375 | 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + | 378 | 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + |
379 | 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + | ||
380 | ') ' + | ||
376 | 'SELECT id FROM children' + | 381 | 'SELECT id FROM children' + |
377 | ')'), | 382 | ')'), |
378 | [ Sequelize.Op.ne ]: comment.id | 383 | [ Sequelize.Op.ne ]: comment.id |
@@ -448,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
448 | } | 453 | } |
449 | } | 454 | } |
450 | 455 | ||
456 | getCommentStaticPath () { | ||
457 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
458 | } | ||
459 | |||
451 | getThreadId (): number { | 460 | getThreadId (): number { |
452 | return this.originCommentId || this.id | 461 | return this.originCommentId || this.id |
453 | } | 462 | } |
@@ -456,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
456 | return this.Account.isOwned() | 465 | return this.Account.isOwned() |
457 | } | 466 | } |
458 | 467 | ||
468 | extractMentions () { | ||
469 | if (!this.text) return [] | ||
470 | |||
471 | const localMention = `@(${actorNameAlphabet}+)` | ||
472 | const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` | ||
473 | |||
474 | const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') | ||
475 | const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') | ||
476 | const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') | ||
477 | const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') | ||
478 | |||
479 | return uniq( | ||
480 | [].concat( | ||
481 | regexpCapture(this.text, remoteMentionsRegex) | ||
482 | .map(([ , username ]) => username), | ||
483 | |||
484 | regexpCapture(this.text, localMentionsRegex) | ||
485 | .map(([ , username ]) => username), | ||
486 | |||
487 | regexpCapture(this.text, firstMentionRegex) | ||
488 | .map(([ , username1, username2 ]) => username1 || username2), | ||
489 | |||
490 | regexpCapture(this.text, endMentionRegex) | ||
491 | .map(([ , username1, username2 ]) => username1 || username2) | ||
492 | ) | ||
493 | ) | ||
494 | } | ||
495 | |||
459 | toFormattedJSON () { | 496 | toFormattedJSON () { |
460 | return { | 497 | return { |
461 | id: this.id, | 498 | id: this.id, |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index adebdf0c7..1f1b76c1e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { values } from 'lodash' | ||
2 | import { | 1 | import { |
3 | AllowNull, | 2 | AllowNull, |
4 | BelongsTo, | 3 | BelongsTo, |
@@ -14,12 +13,12 @@ import { | |||
14 | UpdatedAt | 13 | UpdatedAt |
15 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
16 | import { | 15 | import { |
16 | isVideoFileExtnameValid, | ||
17 | isVideoFileInfoHashValid, | 17 | isVideoFileInfoHashValid, |
18 | isVideoFileResolutionValid, | 18 | isVideoFileResolutionValid, |
19 | isVideoFileSizeValid, | 19 | isVideoFileSizeValid, |
20 | isVideoFPSResolutionValid | 20 | isVideoFPSResolutionValid |
21 | } from '../../helpers/custom-validators/videos' | 21 | } from '../../helpers/custom-validators/videos' |
22 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
23 | import { throwIfNotValid } from '../utils' | 22 | import { throwIfNotValid } from '../utils' |
24 | import { VideoModel } from './video' | 23 | import { VideoModel } from './video' |
25 | import * as Sequelize from 'sequelize' | 24 | import * as Sequelize from 'sequelize' |
@@ -58,7 +57,8 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
58 | size: number | 57 | size: number |
59 | 58 | ||
60 | @AllowNull(false) | 59 | @AllowNull(false) |
61 | @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) | 60 | @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname')) |
61 | @Column | ||
62 | extname: string | 62 | extname: string |
63 | 63 | ||
64 | @AllowNull(false) | 64 | @AllowNull(false) |
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
120 | return VideoFileModel.findById(id, options) | 120 | return VideoFileModel.findById(id, options) |
121 | } | 121 | } |
122 | 122 | ||
123 | static async getStats () { | ||
124 | let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [], | ||
128 | model: VideoModel.unscoped(), | ||
129 | where: { | ||
130 | remote: false | ||
131 | } | ||
132 | } | ||
133 | ] | ||
134 | } as any) | ||
135 | // Sequelize could return null... | ||
136 | if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0 | ||
137 | |||
138 | return { | ||
139 | totalLocalVideoFilesSize | ||
140 | } | ||
141 | } | ||
142 | |||
123 | hasSameUniqueKeysThan (other: VideoFileModel) { | 143 | hasSameUniqueKeysThan (other: VideoFileModel) { |
124 | return this.fps === other.fps && | 144 | return this.fps === other.fps && |
125 | this.resolution === other.resolution && | 145 | this.resolution === other.resolution && |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index e3f8d525b..de0747f22 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -2,7 +2,7 @@ import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | |||
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { VideoFileModel } from './video-file' | 3 | import { VideoFileModel } from './video-file' |
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | 5 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' |
6 | import { VideoCaptionModel } from './video-caption' | 6 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 7 | import { |
8 | getVideoCommentsActivityPubUrl, | 8 | getVideoCommentsActivityPubUrl, |
@@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
207 | for (const file of video.VideoFiles) { | 207 | for (const file of video.VideoFiles) { |
208 | url.push({ | 208 | url.push({ |
209 | type: 'Link', | 209 | type: 'Link', |
210 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 210 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
211 | mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 211 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
212 | href: video.getVideoFileUrl(file, baseUrlHttp), | 212 | href: video.getVideoFileUrl(file, baseUrlHttp), |
213 | height: file.resolution, | 213 | height: file.resolution, |
214 | size: file.size, | 214 | size: file.size, |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8d442b3f8..c723e57c0 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -144,6 +144,10 @@ export class VideoImportModel extends Model<VideoImportModel> { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | getTargetIdentifier () { | ||
148 | return this.targetUrl || this.magnetUri || this.torrentName | ||
149 | } | ||
150 | |||
147 | toFormattedJSON (): VideoImport { | 151 | toFormattedJSON (): VideoImport { |
148 | const videoFormatOptions = { | 152 | const videoFormatOptions = { |
149 | completeDescription: true, | 153 | completeDescription: true, |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0f18d9f0c..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -94,6 +94,7 @@ import { | |||
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | ||
97 | 98 | ||
98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
99 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 100 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -102,17 +103,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ | |||
102 | { fields: [ 'createdAt' ] }, | 103 | { fields: [ 'createdAt' ] }, |
103 | { fields: [ 'publishedAt' ] }, | 104 | { fields: [ 'publishedAt' ] }, |
104 | { fields: [ 'duration' ] }, | 105 | { fields: [ 'duration' ] }, |
105 | { fields: [ 'category' ] }, | ||
106 | { fields: [ 'licence' ] }, | ||
107 | { fields: [ 'nsfw' ] }, | ||
108 | { fields: [ 'language' ] }, | ||
109 | { fields: [ 'waitTranscoding' ] }, | ||
110 | { fields: [ 'state' ] }, | ||
111 | { fields: [ 'remote' ] }, | ||
112 | { fields: [ 'views' ] }, | 106 | { fields: [ 'views' ] }, |
113 | { fields: [ 'likes' ] }, | ||
114 | { fields: [ 'channelId' ] }, | 107 | { fields: [ 'channelId' ] }, |
115 | { | 108 | { |
109 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
110 | where: { | ||
111 | category: { | ||
112 | [Sequelize.Op.ne]: null | ||
113 | } | ||
114 | } | ||
115 | }, | ||
116 | { | ||
117 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
118 | where: { | ||
119 | licence: { | ||
120 | [Sequelize.Op.ne]: null | ||
121 | } | ||
122 | } | ||
123 | }, | ||
124 | { | ||
125 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
126 | where: { | ||
127 | language: { | ||
128 | [Sequelize.Op.ne]: null | ||
129 | } | ||
130 | } | ||
131 | }, | ||
132 | { | ||
133 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
134 | where: { | ||
135 | nsfw: true | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | fields: [ 'remote' ], // Only index local videos | ||
140 | where: { | ||
141 | remote: false | ||
142 | } | ||
143 | }, | ||
144 | { | ||
116 | fields: [ 'uuid' ], | 145 | fields: [ 'uuid' ], |
117 | unique: true | 146 | unique: true |
118 | }, | 147 | }, |
@@ -140,7 +169,7 @@ type ForAPIOptions = { | |||
140 | 169 | ||
141 | type AvailableForListIDsOptions = { | 170 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | 171 | serverAccountId: number |
143 | actorId: number | 172 | followerActorId: number |
144 | includeLocalVideos: boolean | 173 | includeLocalVideos: boolean |
145 | filter?: VideoFilter | 174 | filter?: VideoFilter |
146 | categoryOneOf?: number[] | 175 | categoryOneOf?: number[] |
@@ -153,7 +182,8 @@ type AvailableForListIDsOptions = { | |||
153 | accountId?: number | 182 | accountId?: number |
154 | videoChannelId?: number | 183 | videoChannelId?: number |
155 | trendingDays?: number | 184 | trendingDays?: number |
156 | user?: UserModel | 185 | user?: UserModel, |
186 | historyOfUser?: UserModel | ||
157 | } | 187 | } |
158 | 188 | ||
159 | @Scopes({ | 189 | @Scopes({ |
@@ -315,7 +345,7 @@ type AvailableForListIDsOptions = { | |||
315 | query.include.push(videoChannelInclude) | 345 | query.include.push(videoChannelInclude) |
316 | } | 346 | } |
317 | 347 | ||
318 | if (options.actorId) { | 348 | if (options.followerActorId) { |
319 | let localVideosReq = '' | 349 | let localVideosReq = '' |
320 | if (options.includeLocalVideos === true) { | 350 | if (options.includeLocalVideos === true) { |
321 | localVideosReq = ' UNION ALL ' + | 351 | localVideosReq = ' UNION ALL ' + |
@@ -327,7 +357,7 @@ type AvailableForListIDsOptions = { | |||
327 | } | 357 | } |
328 | 358 | ||
329 | // Force actorId to be a number to avoid SQL injections | 359 | // Force actorId to be a number to avoid SQL injections |
330 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 360 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 361 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
332 | [ Sequelize.Op.in ]: Sequelize.literal( | 362 | [ Sequelize.Op.in ]: Sequelize.literal( |
333 | '(' + | 363 | '(' + |
@@ -416,6 +446,21 @@ type AvailableForListIDsOptions = { | |||
416 | query.subQuery = false | 446 | query.subQuery = false |
417 | } | 447 | } |
418 | 448 | ||
449 | if (options.historyOfUser) { | ||
450 | query.include.push({ | ||
451 | model: UserVideoHistoryModel, | ||
452 | required: true, | ||
453 | where: { | ||
454 | userId: options.historyOfUser.id | ||
455 | } | ||
456 | }) | ||
457 | |||
458 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
459 | // So we won't have multiple rows for the same video | ||
460 | // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel | ||
461 | query.subQuery = false | ||
462 | } | ||
463 | |||
419 | return query | 464 | return query |
420 | }, | 465 | }, |
421 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 466 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
@@ -741,6 +786,15 @@ export class VideoModel extends Model<VideoModel> { | |||
741 | }) | 786 | }) |
742 | VideoBlacklist: VideoBlacklistModel | 787 | VideoBlacklist: VideoBlacklistModel |
743 | 788 | ||
789 | @HasOne(() => VideoImportModel, { | ||
790 | foreignKey: { | ||
791 | name: 'videoId', | ||
792 | allowNull: true | ||
793 | }, | ||
794 | onDelete: 'set null' | ||
795 | }) | ||
796 | VideoImport: VideoImportModel | ||
797 | |||
744 | @HasMany(() => VideoCaptionModel, { | 798 | @HasMany(() => VideoCaptionModel, { |
745 | foreignKey: { | 799 | foreignKey: { |
746 | name: 'videoId', | 800 | name: 'videoId', |
@@ -985,9 +1039,10 @@ export class VideoModel extends Model<VideoModel> { | |||
985 | filter?: VideoFilter, | 1039 | filter?: VideoFilter, |
986 | accountId?: number, | 1040 | accountId?: number, |
987 | videoChannelId?: number, | 1041 | videoChannelId?: number, |
988 | actorId?: number | 1042 | followerActorId?: number |
989 | trendingDays?: number, | 1043 | trendingDays?: number, |
990 | user?: UserModel | 1044 | user?: UserModel, |
1045 | historyOfUser?: UserModel | ||
991 | }, countVideos = true) { | 1046 | }, countVideos = true) { |
992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1047 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
993 | throw new Error('Try to filter all-local but no user has not the see all videos right') | 1048 | throw new Error('Try to filter all-local but no user has not the see all videos right') |
@@ -1008,11 +1063,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1008 | 1063 | ||
1009 | const serverActor = await getServerActor() | 1064 | const serverActor = await getServerActor() |
1010 | 1065 | ||
1011 | // actorId === null has a meaning, so just check undefined | 1066 | // followerActorId === null has a meaning, so just check undefined |
1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id | 1067 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id |
1013 | 1068 | ||
1014 | const queryOptions = { | 1069 | const queryOptions = { |
1015 | actorId, | 1070 | followerActorId, |
1016 | serverAccountId: serverActor.Account.id, | 1071 | serverAccountId: serverActor.Account.id, |
1017 | nsfw: options.nsfw, | 1072 | nsfw: options.nsfw, |
1018 | categoryOneOf: options.categoryOneOf, | 1073 | categoryOneOf: options.categoryOneOf, |
@@ -1026,6 +1081,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1026 | videoChannelId: options.videoChannelId, | 1081 | videoChannelId: options.videoChannelId, |
1027 | includeLocalVideos: options.includeLocalVideos, | 1082 | includeLocalVideos: options.includeLocalVideos, |
1028 | user: options.user, | 1083 | user: options.user, |
1084 | historyOfUser: options.historyOfUser, | ||
1029 | trendingDays | 1085 | trendingDays |
1030 | } | 1086 | } |
1031 | 1087 | ||
@@ -1118,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1118 | 1174 | ||
1119 | const serverActor = await getServerActor() | 1175 | const serverActor = await getServerActor() |
1120 | const queryOptions = { | 1176 | const queryOptions = { |
1121 | actorId: serverActor.id, | 1177 | followerActorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | 1178 | serverAccountId: serverActor.Account.id, |
1123 | includeLocalVideos: options.includeLocalVideos, | 1179 | includeLocalVideos: options.includeLocalVideos, |
1124 | nsfw: options.nsfw, | 1180 | nsfw: options.nsfw, |
@@ -1273,11 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1273 | // threshold corresponds to how many video the field should have to be returned | 1329 | // threshold corresponds to how many video the field should have to be returned |
1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1330 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1275 | const serverActor = await getServerActor() | 1331 | const serverActor = await getServerActor() |
1276 | const actorId = serverActor.id | 1332 | const followerActorId = serverActor.id |
1277 | 1333 | ||
1278 | const scopeOptions: AvailableForListIDsOptions = { | 1334 | const scopeOptions: AvailableForListIDsOptions = { |
1279 | serverAccountId: serverActor.Account.id, | 1335 | serverAccountId: serverActor.Account.id, |
1280 | actorId, | 1336 | followerActorId, |
1281 | includeLocalVideos: true | 1337 | includeLocalVideos: true |
1282 | } | 1338 | } |
1283 | 1339 | ||
@@ -1341,7 +1397,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1341 | } | 1397 | } |
1342 | 1398 | ||
1343 | const [ count, rowsId ] = await Promise.all([ | 1399 | const [ count, rowsId ] = await Promise.all([ |
1344 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), | 1400 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), |
1345 | VideoModel.scope(idsScope).findAll(query) | 1401 | VideoModel.scope(idsScope).findAll(query) |
1346 | ]) | 1402 | ]) |
1347 | const ids = rowsId.map(r => r.id) | 1403 | const ids = rowsId.map(r => r.id) |
@@ -1481,6 +1537,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1481 | videoFile.infoHash = parsedTorrent.infoHash | 1537 | videoFile.infoHash = parsedTorrent.infoHash |
1482 | } | 1538 | } |
1483 | 1539 | ||
1540 | getWatchStaticPath () { | ||
1541 | return '/videos/watch/' + this.uuid | ||
1542 | } | ||
1543 | |||
1484 | getEmbedStaticPath () { | 1544 | getEmbedStaticPath () { |
1485 | return '/videos/embed/' + this.uuid | 1545 | return '/videos/embed/' + this.uuid |
1486 | } | 1546 | } |
@@ -1538,8 +1598,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1598 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1539 | } | 1599 | } |
1540 | 1600 | ||
1541 | removeFile (videoFile: VideoFileModel) { | 1601 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1542 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1602 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1603 | |||
1604 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1543 | return remove(filePath) | 1605 | return remove(filePath) |
1544 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1606 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1545 | } | 1607 | } |
@@ -1617,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1617 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1679 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1618 | } | 1680 | } |
1619 | 1681 | ||
1682 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1683 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1684 | } | ||
1685 | |||
1620 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1686 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1621 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1687 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1622 | } | 1688 | } |