aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account.ts5
-rw-r--r--server/models/account/actor-image.ts100
-rw-r--r--server/models/account/user-notification-setting.ts26
-rw-r--r--server/models/account/user-notification.ts98
-rw-r--r--server/models/account/user.ts20
-rw-r--r--server/models/activitypub/actor-follow.ts7
-rw-r--r--server/models/activitypub/actor.ts87
-rw-r--r--server/models/application/application.ts4
-rw-r--r--server/models/avatar/avatar.ts81
-rw-r--r--server/models/oauth/oauth-token.ts11
-rw-r--r--server/models/redundancy/video-redundancy.ts9
-rw-r--r--server/models/utils.ts8
-rw-r--r--server/models/video/video-channel.ts102
-rw-r--r--server/models/video/video-query-builder.ts5
-rw-r--r--server/models/video/video.ts19
15 files changed, 393 insertions, 189 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index c72f9c63d..312451abe 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -33,7 +33,7 @@ import {
33import { ActorModel } from '../activitypub/actor' 33import { ActorModel } from '../activitypub/actor'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { ApplicationModel } from '../application/application' 35import { ApplicationModel } from '../application/application'
36import { AvatarModel } from '../avatar/avatar' 36import { ActorImageModel } from './actor-image'
37import { ServerModel } from '../server/server' 37import { ServerModel } from '../server/server'
38import { ServerBlocklistModel } from '../server/server-blocklist' 38import { ServerBlocklistModel } from '../server/server-blocklist'
39import { getSort, throwIfNotValid } from '../utils' 39import { getSort, throwIfNotValid } from '../utils'
@@ -82,7 +82,8 @@ export type SummaryOptions = {
82 serverInclude, 82 serverInclude,
83 83
84 { 84 {
85 model: AvatarModel.unscoped(), 85 model: ActorImageModel.unscoped(),
86 as: 'Avatar',
86 required: false 87 required: false
87 } 88 }
88 ] 89 ]
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts
new file mode 100644
index 000000000..ae05b4969
--- /dev/null
+++ b/server/models/account/actor-image.ts
@@ -0,0 +1,100 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { MActorImageFormattable } from '@server/types/models'
5import { ActorImageType } from '@shared/models'
6import { ActorImage } from '../../../shared/models/actors/actor-image.model'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { logger } from '../../helpers/logger'
9import { CONFIG } from '../../initializers/config'
10import { LAZY_STATIC_PATHS } from '../../initializers/constants'
11import { throwIfNotValid } from '../utils'
12
13@Table({
14 tableName: 'actorImage',
15 indexes: [
16 {
17 fields: [ 'filename' ],
18 unique: true
19 }
20 ]
21})
22export class ActorImageModel extends Model {
23
24 @AllowNull(false)
25 @Column
26 filename: string
27
28 @AllowNull(true)
29 @Default(null)
30 @Column
31 height: number
32
33 @AllowNull(true)
34 @Default(null)
35 @Column
36 width: number
37
38 @AllowNull(true)
39 @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
40 @Column
41 fileUrl: string
42
43 @AllowNull(false)
44 @Column
45 onDisk: boolean
46
47 @AllowNull(false)
48 @Column
49 type: ActorImageType
50
51 @CreatedAt
52 createdAt: Date
53
54 @UpdatedAt
55 updatedAt: Date
56
57 @AfterDestroy
58 static removeFilesAndSendDelete (instance: ActorImageModel) {
59 logger.info('Removing actor image file %s.', instance.filename)
60
61 // Don't block the transaction
62 instance.removeImage()
63 .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
64 }
65
66 static loadByName (filename: string) {
67 const query = {
68 where: {
69 filename
70 }
71 }
72
73 return ActorImageModel.findOne(query)
74 }
75
76 toFormattedJSON (this: MActorImageFormattable): ActorImage {
77 return {
78 path: this.getStaticPath(),
79 createdAt: this.createdAt,
80 updatedAt: this.updatedAt
81 }
82 }
83
84 getStaticPath () {
85 if (this.type === ActorImageType.AVATAR) {
86 return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
87 }
88
89 return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
90 }
91
92 getPath () {
93 return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
94 }
95
96 removeImage () {
97 const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
98 return remove(imagePath)
99 }
100}
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index ebab8b6d2..138051528 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -12,10 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
15import { MNotificationSettingFormattable } from '@server/types/models' 16import { MNotificationSettingFormattable } from '@server/types/models'
16import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 17import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' 18import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { clearCacheByUserId } from '../../lib/oauth-model'
19import { throwIfNotValid } from '../utils' 19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user' 20import { UserModel } from './user'
21 21
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
156 @Column 156 @Column
157 abuseNewMessage: UserNotificationSettingValue 157 abuseNewMessage: UserNotificationSettingValue
158 158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
159 @ForeignKey(() => UserModel) 177 @ForeignKey(() => UserModel)
160 @Column 178 @Column
161 userId: number 179 userId: number
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model {
177 @AfterUpdate 195 @AfterUpdate
178 @AfterDestroy 196 @AfterDestroy
179 static removeTokenCache (instance: UserNotificationSettingModel) { 197 static removeTokenCache (instance: UserNotificationSettingModel) {
180 return clearCacheByUserId(instance.userId) 198 return TokensCache.Instance.clearCacheByUserId(instance.userId)
181 } 199 }
182 200
183 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { 201 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
195 newInstanceFollower: this.newInstanceFollower, 213 newInstanceFollower: this.newInstanceFollower,
196 autoInstanceFollowing: this.autoInstanceFollowing, 214 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage, 215 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange 216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
199 } 219 }
200 } 220 }
201} 221}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index add129644..805095002 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -9,7 +9,8 @@ import { VideoAbuseModel } from '../abuse/video-abuse'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
11import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
12import { AvatarModel } from '../avatar/avatar' 12import { ApplicationModel } from '../application/application'
13import { PluginModel } from '../server/plugin'
13import { ServerModel } from '../server/server' 14import { ServerModel } from '../server/server'
14import { getSort, throwIfNotValid } from '../utils' 15import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video' 16import { VideoModel } from '../video/video'
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment' 19import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import' 20import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account' 21import { AccountModel } from './account'
22import { ActorImageModel } from './actor-image'
21import { UserModel } from './user' 23import { UserModel } from './user'
22 24
23enum ScopeNames { 25enum ScopeNames {
@@ -32,7 +34,8 @@ function buildActorWithAvatarInclude () {
32 include: [ 34 include: [
33 { 35 {
34 attributes: [ 'filename' ], 36 attributes: [ 'filename' ],
35 model: AvatarModel.unscoped(), 37 as: 'Avatar',
38 model: ActorImageModel.unscoped(),
36 required: false 39 required: false
37 }, 40 },
38 { 41 {
@@ -96,7 +99,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
96 attributes: [ 'id' ], 99 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(), 100 model: VideoAbuseModel.unscoped(),
98 required: false, 101 required: false,
99 include: [ buildVideoInclude(true) ] 102 include: [ buildVideoInclude(false) ]
100 }, 103 },
101 { 104 {
102 attributes: [ 'id' ], 105 attributes: [ 'id' ],
@@ -106,12 +109,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
106 { 109 {
107 attributes: [ 'id', 'originCommentId' ], 110 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel.unscoped(), 111 model: VideoCommentModel.unscoped(),
109 required: true, 112 required: false,
110 include: [ 113 include: [
111 { 114 {
112 attributes: [ 'id', 'name', 'uuid' ], 115 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(), 116 model: VideoModel.unscoped(),
114 required: true 117 required: false
115 } 118 }
116 ] 119 ]
117 } 120 }
@@ -120,7 +123,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
120 { 123 {
121 model: AccountModel, 124 model: AccountModel,
122 as: 'FlaggedAccount', 125 as: 'FlaggedAccount',
123 required: true, 126 required: false,
124 include: [ buildActorWithAvatarInclude() ] 127 include: [ buildActorWithAvatarInclude() ]
125 } 128 }
126 ] 129 ]
@@ -141,6 +144,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
141 }, 144 },
142 145
143 { 146 {
147 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
148 model: PluginModel.unscoped(),
149 required: false
150 },
151
152 {
153 attributes: [ 'id', 'latestPeerTubeVersion' ],
154 model: ApplicationModel.unscoped(),
155 required: false
156 },
157
158 {
144 attributes: [ 'id', 'state' ], 159 attributes: [ 'id', 'state' ],
145 model: ActorFollowModel.unscoped(), 160 model: ActorFollowModel.unscoped(),
146 required: false, 161 required: false,
@@ -158,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) {
158 }, 173 },
159 { 174 {
160 attributes: [ 'filename' ], 175 attributes: [ 'filename' ],
161 model: AvatarModel.unscoped(), 176 as: 'Avatar',
177 model: ActorImageModel.unscoped(),
162 required: false 178 required: false
163 }, 179 },
164 { 180 {
@@ -251,6 +267,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
251 [Op.ne]: null 267 [Op.ne]: null
252 } 268 }
253 } 269 }
270 },
271 {
272 fields: [ 'pluginId' ],
273 where: {
274 pluginId: {
275 [Op.ne]: null
276 }
277 }
278 },
279 {
280 fields: [ 'applicationId' ],
281 where: {
282 applicationId: {
283 [Op.ne]: null
284 }
285 }
254 } 286 }
255 ] as (ModelIndexesOptions & { where?: WhereOptions })[] 287 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
256}) 288})
@@ -370,6 +402,30 @@ export class UserNotificationModel extends Model {
370 }) 402 })
371 ActorFollow: ActorFollowModel 403 ActorFollow: ActorFollowModel
372 404
405 @ForeignKey(() => PluginModel)
406 @Column
407 pluginId: number
408
409 @BelongsTo(() => PluginModel, {
410 foreignKey: {
411 allowNull: true
412 },
413 onDelete: 'cascade'
414 })
415 Plugin: PluginModel
416
417 @ForeignKey(() => ApplicationModel)
418 @Column
419 applicationId: number
420
421 @BelongsTo(() => ApplicationModel, {
422 foreignKey: {
423 allowNull: true
424 },
425 onDelete: 'cascade'
426 })
427 Application: ApplicationModel
428
373 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { 429 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
374 const where = { userId } 430 const where = { userId }
375 431
@@ -524,6 +580,18 @@ export class UserNotificationModel extends Model {
524 } 580 }
525 : undefined 581 : undefined
526 582
583 const plugin = this.Plugin
584 ? {
585 name: this.Plugin.name,
586 type: this.Plugin.type,
587 latestVersion: this.Plugin.latestVersion
588 }
589 : undefined
590
591 const peertube = this.Application
592 ? { latestVersion: this.Application.latestPeerTubeVersion }
593 : undefined
594
527 return { 595 return {
528 id: this.id, 596 id: this.id,
529 type: this.type, 597 type: this.type,
@@ -535,6 +603,8 @@ export class UserNotificationModel extends Model {
535 videoBlacklist, 603 videoBlacklist,
536 account, 604 account,
537 actorFollow, 605 actorFollow,
606 plugin,
607 peertube,
538 createdAt: this.createdAt.toISOString(), 608 createdAt: this.createdAt.toISOString(),
539 updatedAt: this.updatedAt.toISOString() 609 updatedAt: this.updatedAt.toISOString()
540 } 610 }
@@ -553,17 +623,19 @@ export class UserNotificationModel extends Model {
553 ? { 623 ? {
554 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), 624 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
555 625
556 video: { 626 video: abuse.VideoCommentAbuse.VideoComment.Video
557 id: abuse.VideoCommentAbuse.VideoComment.Video.id, 627 ? {
558 name: abuse.VideoCommentAbuse.VideoComment.Video.name, 628 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
559 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid 629 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
560 } 630 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
631 }
632 : undefined
561 } 633 }
562 : undefined 634 : undefined
563 635
564 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined 636 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
565 637
566 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined 638 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
567 639
568 return { 640 return {
569 id: abuse.id, 641 id: abuse.id,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index c1f22b76a..00c6d73aa 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -21,6 +21,7 @@ import {
21 Table, 21 Table,
22 UpdatedAt 22 UpdatedAt
23} from 'sequelize-typescript' 23} from 'sequelize-typescript'
24import { TokensCache } from '@server/lib/auth/tokens-cache'
24import { 25import {
25 MMyUserFormattable, 26 MMyUserFormattable,
26 MUser, 27 MUser,
@@ -58,7 +59,6 @@ import {
58} from '../../helpers/custom-validators/users' 59} from '../../helpers/custom-validators/users'
59import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 60import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
60import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 61import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
61import { clearCacheByUserId } from '../../lib/oauth-model'
62import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 62import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
63import { ActorModel } from '../activitypub/actor' 63import { ActorModel } from '../activitypub/actor'
64import { ActorFollowModel } from '../activitypub/actor-follow' 64import { ActorFollowModel } from '../activitypub/actor-follow'
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { AccountModel } from './account' 72import { AccountModel } from './account'
73import { UserNotificationSettingModel } from './user-notification-setting' 73import { UserNotificationSettingModel } from './user-notification-setting'
74import { ActorImageModel } from './actor-image'
74 75
75enum ScopeNames { 76enum ScopeNames {
76 FOR_ME_API = 'FOR_ME_API', 77 FOR_ME_API = 'FOR_ME_API',
@@ -97,7 +98,20 @@ enum ScopeNames {
97 model: AccountModel, 98 model: AccountModel,
98 include: [ 99 include: [
99 { 100 {
100 model: VideoChannelModel 101 model: VideoChannelModel.unscoped(),
102 include: [
103 {
104 model: ActorModel,
105 required: true,
106 include: [
107 {
108 model: ActorImageModel,
109 as: 'Banner',
110 required: false
111 }
112 ]
113 }
114 ]
101 }, 115 },
102 { 116 {
103 attributes: [ 'id', 'name', 'type' ], 117 attributes: [ 'id', 'name', 'type' ],
@@ -411,7 +425,7 @@ export class UserModel extends Model {
411 @AfterUpdate 425 @AfterUpdate
412 @AfterDestroy 426 @AfterDestroy
413 static removeTokenCache (instance: UserModel) { 427 static removeTokenCache (instance: UserModel) {
414 return clearCacheByUserId(instance.id) 428 return TokensCache.Instance.clearCacheByUserId(instance.id)
415 } 429 }
416 430
417 static countTotal () { 431 static countTotal () {
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index ce6a4e267..4c5f37620 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model {
248 } 248 }
249 249
250 return ActorFollowModel.findOne(query) 250 return ActorFollowModel.findOne(query)
251 .then(result => {
252 if (result?.ActorFollowing.VideoChannel) {
253 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
254 }
255
256 return result
257 })
258 } 251 }
259 252
260 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { 253 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 3b98e8841..a6c724f26 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -19,7 +19,7 @@ import {
19} from 'sequelize-typescript' 19} from 'sequelize-typescript'
20import { ModelCache } from '@server/models/model-cache' 20import { ModelCache } from '@server/models/model-cache'
21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' 21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
22import { Avatar } from '../../../shared/models/avatars/avatar.model' 22import { ActorImage } from '../../../shared/models/actors/actor-image.model'
23import { activityPubContextify } from '../../helpers/activitypub' 23import { activityPubContextify } from '../../helpers/activitypub'
24import { 24import {
25 isActorFollowersCountValid, 25 isActorFollowersCountValid,
@@ -29,11 +29,19 @@ import {
29 isActorPublicKeyValid 29 isActorPublicKeyValid
30} from '../../helpers/custom-validators/activitypub/actor' 30} from '../../helpers/custom-validators/activitypub/actor'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
33import { 40import {
34 MActor, 41 MActor,
35 MActorAccountChannelId, 42 MActorAccountChannelId,
36 MActorAP, 43 MActorAPAccount,
44 MActorAPChannel,
37 MActorFormattable, 45 MActorFormattable,
38 MActorFull, 46 MActorFull,
39 MActorHost, 47 MActorHost,
@@ -43,7 +51,7 @@ import {
43 MActorWithInboxes 51 MActorWithInboxes
44} from '../../types/models' 52} from '../../types/models'
45import { AccountModel } from '../account/account' 53import { AccountModel } from '../account/account'
46import { AvatarModel } from '../avatar/avatar' 54import { ActorImageModel } from '../account/actor-image'
47import { ServerModel } from '../server/server' 55import { ServerModel } from '../server/server'
48import { isOutdated, throwIfNotValid } from '../utils' 56import { isOutdated, throwIfNotValid } from '../utils'
49import { VideoModel } from '../video/video' 57import { VideoModel } from '../video/video'
@@ -73,7 +81,8 @@ export const unusedActorAttributesForAPI = [
73 required: false 81 required: false
74 }, 82 },
75 { 83 {
76 model: AvatarModel, 84 model: ActorImageModel,
85 as: 'Avatar',
77 required: false 86 required: false
78 } 87 }
79 ] 88 ]
@@ -100,7 +109,13 @@ export const unusedActorAttributesForAPI = [
100 required: false 109 required: false
101 }, 110 },
102 { 111 {
103 model: AvatarModel, 112 model: ActorImageModel,
113 as: 'Avatar',
114 required: false
115 },
116 {
117 model: ActorImageModel,
118 as: 'Banner',
104 required: false 119 required: false
105 } 120 }
106 ] 121 ]
@@ -213,18 +228,35 @@ export class ActorModel extends Model {
213 @UpdatedAt 228 @UpdatedAt
214 updatedAt: Date 229 updatedAt: Date
215 230
216 @ForeignKey(() => AvatarModel) 231 @ForeignKey(() => ActorImageModel)
217 @Column 232 @Column
218 avatarId: number 233 avatarId: number
219 234
220 @BelongsTo(() => AvatarModel, { 235 @ForeignKey(() => ActorImageModel)
236 @Column
237 bannerId: number
238
239 @BelongsTo(() => ActorImageModel, {
221 foreignKey: { 240 foreignKey: {
241 name: 'avatarId',
222 allowNull: true 242 allowNull: true
223 }, 243 },
244 as: 'Avatar',
224 onDelete: 'set null', 245 onDelete: 'set null',
225 hooks: true 246 hooks: true
226 }) 247 })
227 Avatar: AvatarModel 248 Avatar: ActorImageModel
249
250 @BelongsTo(() => ActorImageModel, {
251 foreignKey: {
252 name: 'bannerId',
253 allowNull: true
254 },
255 as: 'Banner',
256 onDelete: 'set null',
257 hooks: true
258 })
259 Banner: ActorImageModel
228 260
229 @HasMany(() => ActorFollowModel, { 261 @HasMany(() => ActorFollowModel, {
230 foreignKey: { 262 foreignKey: {
@@ -496,7 +528,7 @@ export class ActorModel extends Model {
496 } 528 }
497 529
498 toFormattedSummaryJSON (this: MActorSummaryFormattable) { 530 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
499 let avatar: Avatar = null 531 let avatar: ActorImage = null
500 if (this.Avatar) { 532 if (this.Avatar) {
501 avatar = this.Avatar.toFormattedJSON() 533 avatar = this.Avatar.toFormattedJSON()
502 } 534 }
@@ -512,29 +544,51 @@ export class ActorModel extends Model {
512 toFormattedJSON (this: MActorFormattable) { 544 toFormattedJSON (this: MActorFormattable) {
513 const base = this.toFormattedSummaryJSON() 545 const base = this.toFormattedSummaryJSON()
514 546
547 let banner: ActorImage = null
548 if (this.bannerId) {
549 banner = this.Banner.toFormattedJSON()
550 }
551
515 return Object.assign(base, { 552 return Object.assign(base, {
516 id: this.id, 553 id: this.id,
517 hostRedundancyAllowed: this.getRedundancyAllowed(), 554 hostRedundancyAllowed: this.getRedundancyAllowed(),
518 followingCount: this.followingCount, 555 followingCount: this.followingCount,
519 followersCount: this.followersCount, 556 followersCount: this.followersCount,
557 banner,
520 createdAt: this.createdAt, 558 createdAt: this.createdAt,
521 updatedAt: this.updatedAt 559 updatedAt: this.updatedAt
522 }) 560 })
523 } 561 }
524 562
525 toActivityPubObject (this: MActorAP, name: string) { 563 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
526 let icon: ActivityIconObject 564 let icon: ActivityIconObject
565 let image: ActivityIconObject
527 566
528 if (this.avatarId) { 567 if (this.avatarId) {
529 const extension = extname(this.Avatar.filename) 568 const extension = extname(this.Avatar.filename)
530 569
531 icon = { 570 icon = {
532 type: 'Image', 571 type: 'Image',
533 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', 572 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
573 height: this.Avatar.height,
574 width: this.Avatar.width,
534 url: this.getAvatarUrl() 575 url: this.getAvatarUrl()
535 } 576 }
536 } 577 }
537 578
579 if (this.bannerId) {
580 const banner = (this as MActorAPChannel).Banner
581 const extension = extname(banner.filename)
582
583 image = {
584 type: 'Image',
585 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
586 height: banner.height,
587 width: banner.width,
588 url: this.getBannerUrl()
589 }
590 }
591
538 const json = { 592 const json = {
539 type: this.type, 593 type: this.type,
540 id: this.url, 594 id: this.url,
@@ -554,7 +608,8 @@ export class ActorModel extends Model {
554 owner: this.url, 608 owner: this.url,
555 publicKeyPem: this.publicKey 609 publicKeyPem: this.publicKey
556 }, 610 },
557 icon 611 icon,
612 image
558 } 613 }
559 614
560 return activityPubContextify(json) 615 return activityPubContextify(json)
@@ -624,6 +679,12 @@ export class ActorModel extends Model {
624 return WEBSERVER.URL + this.Avatar.getStaticPath() 679 return WEBSERVER.URL + this.Avatar.getStaticPath()
625 } 680 }
626 681
682 getBannerUrl () {
683 if (!this.bannerId) return undefined
684
685 return WEBSERVER.URL + this.Banner.getStaticPath()
686 }
687
627 isOutdated () { 688 isOutdated () {
628 if (this.isOwned()) return false 689 if (this.isOwned()) return false
629 690
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 909569de1..21f8b1cbc 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
32 @Column 32 @Column
33 migrationVersion: number 33 migrationVersion: number
34 34
35 @AllowNull(true)
36 @Column
37 latestPeerTubeVersion: string
38
35 @HasOne(() => AccountModel, { 39 @HasOne(() => AccountModel, {
36 foreignKey: { 40 foreignKey: {
37 allowNull: true 41 allowNull: true
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
deleted file mode 100644
index 0d246a144..000000000
--- a/server/models/avatar/avatar.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import { join } from 'path'
2import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model'
4import { LAZY_STATIC_PATHS } from '../../initializers/constants'
5import { logger } from '../../helpers/logger'
6import { remove } from 'fs-extra'
7import { CONFIG } from '../../initializers/config'
8import { throwIfNotValid } from '../utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { MAvatarFormattable } from '@server/types/models'
11
12@Table({
13 tableName: 'avatar',
14 indexes: [
15 {
16 fields: [ 'filename' ],
17 unique: true
18 }
19 ]
20})
21export class AvatarModel extends Model {
22
23 @AllowNull(false)
24 @Column
25 filename: string
26
27 @AllowNull(true)
28 @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
29 @Column
30 fileUrl: string
31
32 @AllowNull(false)
33 @Column
34 onDisk: boolean
35
36 @CreatedAt
37 createdAt: Date
38
39 @UpdatedAt
40 updatedAt: Date
41
42 @AfterDestroy
43 static removeFilesAndSendDelete (instance: AvatarModel) {
44 logger.info('Removing avatar file %s.', instance.filename)
45
46 // Don't block the transaction
47 instance.removeAvatar()
48 .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err))
49 }
50
51 static loadByName (filename: string) {
52 const query = {
53 where: {
54 filename
55 }
56 }
57
58 return AvatarModel.findOne(query)
59 }
60
61 toFormattedJSON (this: MAvatarFormattable): Avatar {
62 return {
63 path: this.getStaticPath(),
64 createdAt: this.createdAt,
65 updatedAt: this.updatedAt
66 }
67 }
68
69 getStaticPath () {
70 return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
71 }
72
73 getPath () {
74 return join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
75 }
76
77 removeAvatar () {
78 const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
79 return remove(avatarPath)
80 }
81}
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 6bc6cf27c..27e643aa7 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -12,9 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
16import { MUserAccountId } from '@server/types/models'
15import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 17import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
16import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
17import { clearCacheByToken } from '../../lib/oauth-model'
18import { AccountModel } from '../account/account' 19import { AccountModel } from '../account/account'
19import { UserModel } from '../account/user' 20import { UserModel } from '../account/user'
20import { ActorModel } from '../activitypub/actor' 21import { ActorModel } from '../activitypub/actor'
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = {
26 client: { 27 client: {
27 id: number 28 id: number
28 } 29 }
29 user: { 30 user: MUserAccountId
30 id: number
31 }
32 token: MOAuthTokenUser 31 token: MOAuthTokenUser
33} 32}
34 33
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model {
133 @AfterUpdate 132 @AfterUpdate
134 @AfterDestroy 133 @AfterDestroy
135 static removeTokenCache (token: OAuthTokenModel) { 134 static removeTokenCache (token: OAuthTokenModel) {
136 return clearCacheByToken(token.accessToken) 135 return TokensCache.Instance.clearCacheByToken(token.accessToken)
137 } 136 }
138 137
139 static loadByRefreshToken (refreshToken: string) { 138 static loadByRefreshToken (refreshToken: string) {
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model {
206 } 205 }
207 206
208 static deleteUserToken (userId: number, t?: Transaction) { 207 static deleteUserToken (userId: number, t?: Transaction) {
208 TokensCache.Instance.deleteUserToken(userId)
209
209 const query = { 210 const query = {
210 where: { 211 where: {
211 userId 212 userId
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 53293df37..53ebadeaf 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -32,6 +32,7 @@ import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
32import { ActorModel } from '../activitypub/actor' 32import { ActorModel } from '../activitypub/actor'
33import { ServerModel } from '../server/server' 33import { ServerModel } from '../server/server'
34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' 34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
35import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
35import { VideoModel } from '../video/video' 36import { VideoModel } from '../video/video'
36import { VideoChannelModel } from '../video/video-channel' 37import { VideoChannelModel } from '../video/video-channel'
37import { VideoFileModel } from '../video/video-file' 38import { VideoFileModel } from '../video/video-file'
@@ -374,7 +375,13 @@ export class VideoRedundancyModel extends Model {
374 ...this.buildVideoIdsForDuplication(peertubeActor) 375 ...this.buildVideoIdsForDuplication(peertubeActor)
375 }, 376 },
376 include: [ 377 include: [
377 VideoRedundancyModel.buildServerRedundancyInclude() 378 VideoRedundancyModel.buildServerRedundancyInclude(),
379
380 // Required by publishedAt sort
381 {
382 model: ScheduleVideoUpdateModel.unscoped(),
383 required: false
384 }
378 ] 385 ]
379 } 386 }
380 387
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 5337ae75d..ec51c66bf 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -56,6 +56,14 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or
56 56
57 lastSort 57 lastSort
58 ] 58 ]
59 } else if (field === 'publishedAt') {
60 return [
61 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
62
63 [ Sequelize.col('VideoModel.publishedAt'), direction ],
64
65 lastSort
66 ]
59 } 67 }
60 68
61 let finalField: string | Col 69 let finalField: string | Col
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 178878c55..d2a055f5b 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -28,17 +28,16 @@ import {
28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
29import { sendDeleteActor } from '../../lib/activitypub/send' 29import { sendDeleteActor } from '../../lib/activitypub/send'
30import { 30import {
31 MChannelAccountDefault,
32 MChannelActor, 31 MChannelActor,
33 MChannelActorAccountDefaultVideos,
34 MChannelAP, 32 MChannelAP,
33 MChannelBannerAccountDefault,
35 MChannelFormattable, 34 MChannelFormattable,
36 MChannelSummaryFormattable 35 MChannelSummaryFormattable
37} from '../../types/models/video' 36} from '../../types/models/video'
38import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' 37import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
38import { ActorImageModel } from '../account/actor-image'
39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
40import { ActorFollowModel } from '../activitypub/actor-follow' 40import { ActorFollowModel } from '../activitypub/actor-follow'
41import { AvatarModel } from '../avatar/avatar'
42import { ServerModel } from '../server/server' 41import { ServerModel } from '../server/server'
43import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 42import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
44import { VideoModel } from './video' 43import { VideoModel } from './video'
@@ -49,6 +48,7 @@ export enum ScopeNames {
49 SUMMARY = 'SUMMARY', 48 SUMMARY = 'SUMMARY',
50 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
51 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
51 WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER',
52 WITH_VIDEOS = 'WITH_VIDEOS', 52 WITH_VIDEOS = 'WITH_VIDEOS',
53 WITH_STATS = 'WITH_STATS' 53 WITH_STATS = 'WITH_STATS'
54} 54}
@@ -99,7 +99,14 @@ export type SummaryOptions = {
99 } 99 }
100 } 100 }
101 ] 101 ]
102 } 102 },
103 include: [
104 {
105 model: ActorImageModel,
106 as: 'Banner',
107 required: false
108 }
109 ]
103 }, 110 },
104 { 111 {
105 model: AccountModel, 112 model: AccountModel,
@@ -130,7 +137,8 @@ export type SummaryOptions = {
130 required: false 137 required: false
131 }, 138 },
132 { 139 {
133 model: AvatarModel.unscoped(), 140 model: ActorImageModel.unscoped(),
141 as: 'Avatar',
134 required: false 142 required: false
135 } 143 }
136 ] 144 ]
@@ -167,6 +175,20 @@ export type SummaryOptions = {
167 ActorModel 175 ActorModel
168 ] 176 ]
169 }, 177 },
178 [ScopeNames.WITH_ACTOR_BANNER]: {
179 include: [
180 {
181 model: ActorModel,
182 include: [
183 {
184 model: ActorImageModel,
185 required: false,
186 as: 'Banner'
187 }
188 ]
189 }
190 ]
191 },
170 [ScopeNames.WITH_VIDEOS]: { 192 [ScopeNames.WITH_VIDEOS]: {
171 include: [ 193 include: [
172 VideoModel 194 VideoModel
@@ -441,7 +463,7 @@ export class VideoChannelModel extends Model {
441 where 463 where
442 } 464 }
443 465
444 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] 466 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
445 467
446 if (options.withStats === true) { 468 if (options.withStats === true) {
447 scopes.push({ 469 scopes.push({
@@ -457,32 +479,13 @@ export class VideoChannelModel extends Model {
457 }) 479 })
458 } 480 }
459 481
460 static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { 482 static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
461 return VideoChannelModel.unscoped() 483 return VideoChannelModel.unscoped()
462 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 484 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
463 .findByPk(id) 485 .findByPk(id)
464 } 486 }
465 487
466 static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { 488 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
467 const query = {
468 where: {
469 id,
470 accountId
471 }
472 }
473
474 return VideoChannelModel.unscoped()
475 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
476 .findOne(query)
477 }
478
479 static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
480 return VideoChannelModel.unscoped()
481 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
482 .findByPk(id)
483 }
484
485 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> {
486 const query = { 489 const query = {
487 include: [ 490 include: [
488 { 491 {
@@ -490,7 +493,14 @@ export class VideoChannelModel extends Model {
490 required: true, 493 required: true,
491 where: { 494 where: {
492 url 495 url
493 } 496 },
497 include: [
498 {
499 model: ActorImageModel,
500 required: false,
501 as: 'Banner'
502 }
503 ]
494 } 504 }
495 ] 505 ]
496 } 506 }
@@ -508,7 +518,7 @@ export class VideoChannelModel extends Model {
508 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) 518 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
509 } 519 }
510 520
511 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { 521 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> {
512 const query = { 522 const query = {
513 include: [ 523 include: [
514 { 524 {
@@ -517,17 +527,24 @@ export class VideoChannelModel extends Model {
517 where: { 527 where: {
518 preferredUsername: name, 528 preferredUsername: name,
519 serverId: null 529 serverId: null
520 } 530 },
531 include: [
532 {
533 model: ActorImageModel,
534 required: false,
535 as: 'Banner'
536 }
537 ]
521 } 538 }
522 ] 539 ]
523 } 540 }
524 541
525 return VideoChannelModel.unscoped() 542 return VideoChannelModel.unscoped()
526 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 543 .scope([ ScopeNames.WITH_ACCOUNT ])
527 .findOne(query) 544 .findOne(query)
528 } 545 }
529 546
530 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { 547 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> {
531 const query = { 548 const query = {
532 include: [ 549 include: [
533 { 550 {
@@ -541,6 +558,11 @@ export class VideoChannelModel extends Model {
541 model: ServerModel, 558 model: ServerModel,
542 required: true, 559 required: true,
543 where: { host } 560 where: { host }
561 },
562 {
563 model: ActorImageModel,
564 required: false,
565 as: 'Banner'
544 } 566 }
545 ] 567 ]
546 } 568 }
@@ -548,22 +570,10 @@ export class VideoChannelModel extends Model {
548 } 570 }
549 571
550 return VideoChannelModel.unscoped() 572 return VideoChannelModel.unscoped()
551 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 573 .scope([ ScopeNames.WITH_ACCOUNT ])
552 .findOne(query) 574 .findOne(query)
553 } 575 }
554 576
555 static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> {
556 const options = {
557 include: [
558 VideoModel
559 ]
560 }
561
562 return VideoChannelModel.unscoped()
563 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
564 .findByPk(id, options)
565 }
566
567 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { 577 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
568 const actor = this.Actor.toFormattedSummaryJSON() 578 const actor = this.Actor.toFormattedSummaryJSON()
569 579
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 96df0a7f8..4d95ddee2 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', 490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
491 491
492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', 492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
493 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', 493 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
494 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
494 495
495 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + 496 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
496 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', 497 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
497 498
498 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + 499 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
499 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', 500 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
500 501
501 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' 502 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3c4f3d3df..e9afb2c18 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,7 +24,6 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { v4 as uuidv4 } from 'uuid'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live-manager' 29import { LiveManager } from '@server/lib/live-manager'
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
100import { VideoAbuseModel } from '../abuse/video-abuse' 99import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account' 100import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate' 101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { ActorImageModel } from '../account/actor-image'
103import { UserModel } from '../account/user' 103import { UserModel } from '../account/user'
104import { UserVideoHistoryModel } from '../account/user-video-history' 104import { UserVideoHistoryModel } from '../account/user-video-history'
105import { ActorModel } from '../activitypub/actor' 105import { ActorModel } from '../activitypub/actor'
106import { AvatarModel } from '../avatar/avatar'
107import { VideoRedundancyModel } from '../redundancy/video-redundancy' 106import { VideoRedundancyModel } from '../redundancy/video-redundancy'
108import { ServerModel } from '../server/server' 107import { ServerModel } from '../server/server'
109import { TrackerModel } from '../server/tracker' 108import { TrackerModel } from '../server/tracker'
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = {
286 required: false 285 required: false
287 }, 286 },
288 { 287 {
289 model: AvatarModel.unscoped(), 288 model: ActorImageModel.unscoped(),
289 as: 'Avatar',
290 required: false 290 required: false
291 } 291 }
292 ] 292 ]
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = {
308 required: false 308 required: false
309 }, 309 },
310 { 310 {
311 model: AvatarModel.unscoped(), 311 model: ActorImageModel.unscoped(),
312 as: 'Avatar',
312 required: false 313 required: false
313 } 314 }
314 ] 315 ]
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model {
1703 1704
1704 function buildActor (rowActor: any) { 1705 function buildActor (rowActor: any) {
1705 const avatarModel = rowActor.Avatar.id !== null 1706 const avatarModel = rowActor.Avatar.id !== null
1706 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) 1707 ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
1707 : null 1708 : null
1708 1709
1709 const serverModel = rowActor.Server.id !== null 1710 const serverModel = rowActor.Server.id !== null
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model {
1869 this.Thumbnails.push(savedThumbnail) 1870 this.Thumbnails.push(savedThumbnail)
1870 } 1871 }
1871 1872
1872 generateThumbnailName () {
1873 return uuidv4() + '.jpg'
1874 }
1875
1876 getMiniature () { 1873 getMiniature () {
1877 if (Array.isArray(this.Thumbnails) === false) return undefined 1874 if (Array.isArray(this.Thumbnails) === false) return undefined
1878 1875
1879 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) 1876 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1880 } 1877 }
1881 1878
1882 generatePreviewName () {
1883 return uuidv4() + '.jpg'
1884 }
1885
1886 hasPreview () { 1879 hasPreview () {
1887 return !!this.getPreview() 1880 return !!this.getPreview()
1888 } 1881 }