diff options
author | kontrollanten <6680299+kontrollanten@users.noreply.github.com> | 2022-02-28 08:34:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-28 08:34:43 +0100 |
commit | d0800f7661f13fabe7bb6f4aa0ea50764f106405 (patch) | |
tree | d43e6b0b6f4a5a32e03487e6464edbcaf288be2a /server | |
parent | 5cad2ca9db9b9d138f8a33058d10b94a9fd50c69 (diff) | |
download | PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.gz PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.zst PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.zip |
Implement avatar miniatures (#4639)
* client: remove unused file
* refactor(client/my-actor-avatar): size from input
Read size from component input instead of scss, to make it possible to
use smaller avatar images when implemented.
* implement avatar miniatures
close #4560
* fix(test): max file size
* fix(search-index): normalize res acc to avatarMini
* refactor avatars to an array
* client/search: resize channel avatar to 120
* refactor(client/videos): remove unused function
* client(actor-avatar): set default size
* fix tests and avatars full result
When findOne is used only an array containting one avatar is returned.
* update migration version and version notations
* server/search: harmonize normalizing
* Cleanup avatar miniature PR
Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server')
84 files changed, 1664 insertions, 989 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 4e6bd5e25..c4d1be121 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -18,10 +18,10 @@ import { | |||
18 | } from '../../lib/activitypub/url' | 18 | } from '../../lib/activitypub/url' |
19 | import { | 19 | import { |
20 | asyncMiddleware, | 20 | asyncMiddleware, |
21 | ensureIsLocalChannel, | ||
21 | executeIfActivityPub, | 22 | executeIfActivityPub, |
22 | localAccountValidator, | 23 | localAccountValidator, |
23 | videoChannelsNameWithHostValidator, | 24 | videoChannelsNameWithHostValidator, |
24 | ensureIsLocalChannel, | ||
25 | videosCustomGetValidator, | 25 | videosCustomGetValidator, |
26 | videosShareValidator | 26 | videosShareValidator |
27 | } from '../../middlewares' | 27 | } from '../../middlewares' |
@@ -265,8 +265,8 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp | |||
265 | const handler = async (start: number, count: number) => { | 265 | const handler = async (start: number, count: number) => { |
266 | const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) | 266 | const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) |
267 | return { | 267 | return { |
268 | total: result.count, | 268 | total: result.total, |
269 | data: result.rows.map(r => r.url) | 269 | data: result.data.map(r => r.url) |
270 | } | 270 | } |
271 | } | 271 | } |
272 | const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) | 272 | const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) |
@@ -301,9 +301,10 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
301 | 301 | ||
302 | const handler = async (start: number, count: number) => { | 302 | const handler = async (start: number, count: number) => { |
303 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) | 303 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) |
304 | |||
304 | return { | 305 | return { |
305 | total: result.count, | 306 | total: result.total, |
306 | data: result.rows.map(r => r.url) | 307 | data: result.data.map(r => r.url) |
307 | } | 308 | } |
308 | } | 309 | } |
309 | const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) | 310 | const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) |
@@ -425,8 +426,8 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: MVide | |||
425 | const handler = async (start: number, count: number) => { | 426 | const handler = async (start: number, count: number) => { |
426 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) | 427 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) |
427 | return { | 428 | return { |
428 | total: result.count, | 429 | total: result.total, |
429 | data: result.rows.map(r => r.url) | 430 | data: result.data.map(r => r.url) |
430 | } | 431 | } |
431 | } | 432 | } |
432 | return activityPubCollectionPagination(url, handler, req.query.page) | 433 | return activityPubCollectionPagination(url, handler, req.query.page) |
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 46d89bafa..8d9f92d93 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -213,7 +213,7 @@ async function listAccountRatings (req: express.Request, res: express.Response) | |||
213 | sort: req.query.sort, | 213 | sort: req.query.sort, |
214 | type: req.query.rating | 214 | type: req.query.rating |
215 | }) | 215 | }) |
216 | return res.json(getFormattedObjects(resultList.rows, resultList.count)) | 216 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
217 | } | 217 | } |
218 | 218 | ||
219 | async function listAccountFollowers (req: express.Request, res: express.Response) { | 219 | async function listAccountFollowers (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index c2ad0b710..a1d621152 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' | 3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' |
4 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 5 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { pick } from '@shared/core-utils' | ||
5 | import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models' | 7 | import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 8 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { createReqFiles } from '../../../helpers/express-utils' | 9 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -10,7 +12,7 @@ import { CONFIG } from '../../../initializers/config' | |||
10 | import { MIMETYPES } from '../../../initializers/constants' | 12 | import { MIMETYPES } from '../../../initializers/constants' |
11 | import { sequelizeTypescript } from '../../../initializers/database' | 13 | import { sequelizeTypescript } from '../../../initializers/database' |
12 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 14 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
13 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor' | 15 | import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor' |
14 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 16 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
15 | import { | 17 | import { |
16 | asyncMiddleware, | 18 | asyncMiddleware, |
@@ -30,7 +32,6 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
30 | import { UserModel } from '../../../models/user/user' | 32 | import { UserModel } from '../../../models/user/user' |
31 | import { VideoModel } from '../../../models/video/video' | 33 | import { VideoModel } from '../../../models/video/video' |
32 | import { VideoImportModel } from '../../../models/video/video-import' | 34 | import { VideoImportModel } from '../../../models/video/video-import' |
33 | import { pick } from '@shared/core-utils' | ||
34 | 35 | ||
35 | const auditLogger = auditLoggerFactory('users') | 36 | const auditLogger = auditLoggerFactory('users') |
36 | 37 | ||
@@ -253,9 +254,17 @@ async function updateMyAvatar (req: express.Request, res: express.Response) { | |||
253 | 254 | ||
254 | const userAccount = await AccountModel.load(user.Account.id) | 255 | const userAccount = await AccountModel.load(user.Account.id) |
255 | 256 | ||
256 | const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR) | 257 | const avatars = await updateLocalActorImageFiles( |
258 | userAccount, | ||
259 | avatarPhysicalFile, | ||
260 | ActorImageType.AVATAR | ||
261 | ) | ||
257 | 262 | ||
258 | return res.json({ avatar: avatar.toFormattedJSON() }) | 263 | return res.json({ |
264 | // TODO: remove, deprecated in 4.2 | ||
265 | avatar: getBiggestActorImage(avatars).toFormattedJSON(), | ||
266 | avatars: avatars.map(avatar => avatar.toFormattedJSON()) | ||
267 | }) | ||
259 | } | 268 | } |
260 | 269 | ||
261 | async function deleteMyAvatar (req: express.Request, res: express.Response) { | 270 | async function deleteMyAvatar (req: express.Request, res: express.Response) { |
@@ -264,5 +273,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { | |||
264 | const userAccount = await AccountModel.load(user.Account.id) | 273 | const userAccount = await AccountModel.load(user.Account.id) |
265 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) | 274 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) |
266 | 275 | ||
267 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 276 | return res.json({ avatars: [] }) |
268 | } | 277 | } |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index d107a306e..58732158f 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -3,7 +3,6 @@ import express from 'express' | |||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | 3 | import { UserNotificationModel } from '@server/models/user/user-notification' |
4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
5 | import { UserNotificationSetting } from '../../../../shared/models/users' | 5 | import { UserNotificationSetting } from '../../../../shared/models/users' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | ||
7 | import { | 6 | import { |
8 | asyncMiddleware, | 7 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 8 | asyncRetryTransactionMiddleware, |
@@ -20,6 +19,7 @@ import { | |||
20 | } from '../../../middlewares/validators/user-notifications' | 19 | } from '../../../middlewares/validators/user-notifications' |
21 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' | 20 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' |
22 | import { meRouter } from './me' | 21 | import { meRouter } from './me' |
22 | import { getFormattedObjects } from '@server/helpers/utils' | ||
23 | 23 | ||
24 | const myNotificationsRouter = express.Router() | 24 | const myNotificationsRouter = express.Router() |
25 | 25 | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index e65550a22..2f869d9b3 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
3 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
@@ -16,7 +17,7 @@ import { MIMETYPES } from '../../initializers/constants' | |||
16 | import { sequelizeTypescript } from '../../initializers/database' | 17 | import { sequelizeTypescript } from '../../initializers/database' |
17 | import { sendUpdateActor } from '../../lib/activitypub/send' | 18 | import { sendUpdateActor } from '../../lib/activitypub/send' |
18 | import { JobQueue } from '../../lib/job-queue' | 19 | import { JobQueue } from '../../lib/job-queue' |
19 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor' | 20 | import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor' |
20 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 21 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
21 | import { | 22 | import { |
22 | asyncMiddleware, | 23 | asyncMiddleware, |
@@ -186,11 +187,15 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp | |||
186 | const videoChannel = res.locals.videoChannel | 187 | const videoChannel = res.locals.videoChannel |
187 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 188 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
188 | 189 | ||
189 | const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) | 190 | const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) |
190 | 191 | ||
191 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 192 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
192 | 193 | ||
193 | return res.json({ banner: banner.toFormattedJSON() }) | 194 | return res.json({ |
195 | // TODO: remove, deprecated in 4.2 | ||
196 | banner: getBiggestActorImage(banners).toFormattedJSON(), | ||
197 | banners: banners.map(b => b.toFormattedJSON()) | ||
198 | }) | ||
194 | } | 199 | } |
195 | 200 | ||
196 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 201 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
@@ -198,11 +203,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp | |||
198 | const videoChannel = res.locals.videoChannel | 203 | const videoChannel = res.locals.videoChannel |
199 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 204 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
200 | 205 | ||
201 | const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) | 206 | const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) |
202 | |||
203 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 207 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
204 | 208 | ||
205 | return res.json({ avatar: avatar.toFormattedJSON() }) | 209 | return res.json({ |
210 | // TODO: remove, deprecated in 4.2 | ||
211 | avatar: getBiggestActorImage(avatars).toFormattedJSON(), | ||
212 | avatars: avatars.map(a => a.toFormattedJSON()) | ||
213 | }) | ||
206 | } | 214 | } |
207 | 215 | ||
208 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | 216 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 8a56f2f75..f9514d988 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -68,7 +68,9 @@ const staticClientOverrides = [ | |||
68 | 'assets/images/icons/icon-512x512.png', | 68 | 'assets/images/icons/icon-512x512.png', |
69 | 'assets/images/default-playlist.jpg', | 69 | 'assets/images/default-playlist.jpg', |
70 | 'assets/images/default-avatar-account.png', | 70 | 'assets/images/default-avatar-account.png', |
71 | 'assets/images/default-avatar-video-channel.png' | 71 | 'assets/images/default-avatar-account-48x48.png', |
72 | 'assets/images/default-avatar-video-channel.png', | ||
73 | 'assets/images/default-avatar-video-channel-48x48.png' | ||
72 | ] | 74 | ] |
73 | 75 | ||
74 | for (const staticClientOverride of staticClientOverrides) { | 76 | for (const staticClientOverride of staticClientOverrides) { |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index a4076ee56..55bf02660 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -64,7 +64,15 @@ async function getActorImage (req: express.Request, res: express.Response, next: | |||
64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | 64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
65 | 65 | ||
66 | try { | 66 | try { |
67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) | 67 | await pushActorImageProcessInQueue({ |
68 | filename: image.filename, | ||
69 | fileUrl: image.fileUrl, | ||
70 | size: { | ||
71 | height: image.height, | ||
72 | width: image.width | ||
73 | }, | ||
74 | type: image.type | ||
75 | }) | ||
68 | } catch (err) { | 76 | } catch (err) { |
69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) | 77 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) |
70 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | 78 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index fe721cbac..cbba2f51c 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -38,6 +38,9 @@ function getContextData (type: ContextType) { | |||
38 | sensitive: 'as:sensitive', | 38 | sensitive: 'as:sensitive', |
39 | language: 'sc:inLanguage', | 39 | language: 'sc:inLanguage', |
40 | 40 | ||
41 | // TODO: remove in a few versions, introduced in 4.2 | ||
42 | icons: 'as:icon', | ||
43 | |||
41 | isLiveBroadcast: 'sc:isLiveBroadcast', | 44 | isLiveBroadcast: 'sc:isLiveBroadcast', |
42 | liveSaveReplay: { | 45 | liveSaveReplay: { |
43 | '@type': 'sc:Boolean', | 46 | '@type': 'sc:Boolean', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 1c47d43f0..9b972b87e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | VideoTranscodingFPS | 14 | VideoTranscodingFPS |
15 | } from '../../shared/models' | 15 | } from '../../shared/models' |
16 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 16 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
17 | import { FollowState } from '../../shared/models/actors' | 17 | import { ActorImageType, FollowState } from '../../shared/models/actors' |
18 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 18 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
19 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' | 19 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' |
20 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' | 20 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' |
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 680 | 27 | const LAST_MIGRATION_VERSION = 685 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -633,15 +633,23 @@ const PREVIEWS_SIZE = { | |||
633 | height: 480, | 633 | height: 480, |
634 | minWidth: 400 | 634 | minWidth: 400 |
635 | } | 635 | } |
636 | const ACTOR_IMAGES_SIZE = { | 636 | const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: number }[]} = { |
637 | AVATARS: { | 637 | [ActorImageType.AVATAR]: [ |
638 | width: 120, | 638 | { |
639 | height: 120 | 639 | width: 120, |
640 | }, | 640 | height: 120 |
641 | BANNERS: { | 641 | }, |
642 | width: 1920, | 642 | { |
643 | height: 317 // 6/1 ratio | 643 | width: 48, |
644 | } | 644 | height: 48 |
645 | } | ||
646 | ], | ||
647 | [ActorImageType.BANNER]: [ | ||
648 | { | ||
649 | width: 1920, | ||
650 | height: 317 // 6/1 ratio | ||
651 | } | ||
652 | ] | ||
645 | } | 653 | } |
646 | 654 | ||
647 | const EMBED_SIZE = { | 655 | const EMBED_SIZE = { |
diff --git a/server/initializers/migrations/0685-multiple-actor-images.ts b/server/initializers/migrations/0685-multiple-actor-images.ts new file mode 100644 index 000000000..c656f7e28 --- /dev/null +++ b/server/initializers/migrations/0685-multiple-actor-images.ts | |||
@@ -0,0 +1,62 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | await utils.queryInterface.addColumn('actorImage', 'actorId', { | ||
11 | type: Sequelize.INTEGER, | ||
12 | defaultValue: null, | ||
13 | allowNull: true, | ||
14 | references: { | ||
15 | model: 'actor', | ||
16 | key: 'id' | ||
17 | }, | ||
18 | onDelete: 'CASCADE' | ||
19 | }, { transaction: utils.transaction }) | ||
20 | |||
21 | // Avatars | ||
22 | { | ||
23 | const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."avatarId" = "actorImage"."id") ` + | ||
24 | `WHERE "type" = 1` | ||
25 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction }) | ||
26 | } | ||
27 | |||
28 | // Banners | ||
29 | { | ||
30 | const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."bannerId" = "actorImage"."id") ` + | ||
31 | `WHERE "type" = 2` | ||
32 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction }) | ||
33 | } | ||
34 | |||
35 | // Remove orphans | ||
36 | { | ||
37 | const query = `DELETE FROM "actorImage" WHERE id NOT IN (` + | ||
38 | `SELECT "bannerId" FROM actor WHERE "bannerId" IS NOT NULL ` + | ||
39 | `UNION select "avatarId" FROM actor WHERE "avatarId" IS NOT NULL` + | ||
40 | `);` | ||
41 | |||
42 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.DELETE, transaction: utils.transaction }) | ||
43 | } | ||
44 | |||
45 | await utils.queryInterface.changeColumn('actorImage', 'actorId', { | ||
46 | type: Sequelize.INTEGER, | ||
47 | allowNull: false | ||
48 | }, { transaction: utils.transaction }) | ||
49 | |||
50 | await utils.queryInterface.removeColumn('actor', 'avatarId', { transaction: utils.transaction }) | ||
51 | await utils.queryInterface.removeColumn('actor', 'bannerId', { transaction: utils.transaction }) | ||
52 | } | ||
53 | } | ||
54 | |||
55 | function down () { | ||
56 | throw new Error('Not implemented.') | ||
57 | } | ||
58 | |||
59 | export { | ||
60 | up, | ||
61 | down | ||
62 | } | ||
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts index 443ad0a63..d17c2ef1a 100644 --- a/server/lib/activitypub/actors/image.ts +++ b/server/lib/activitypub/actors/image.ts | |||
@@ -12,53 +12,52 @@ type ImageInfo = { | |||
12 | onDisk?: boolean | 12 | onDisk?: boolean |
13 | } | 13 | } |
14 | 14 | ||
15 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | 15 | async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) { |
16 | const oldImageModel = type === ActorImageType.AVATAR | 16 | const avatarsOrBanners = type === ActorImageType.AVATAR |
17 | ? actor.Avatar | 17 | ? actor.Avatars |
18 | : actor.Banner | 18 | : actor.Banners |
19 | 19 | ||
20 | if (oldImageModel) { | 20 | if (imagesInfo.length === 0) { |
21 | // Don't update the avatar if the file URL did not change | 21 | await deleteActorImages(actor, type, t) |
22 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor | 22 | } |
23 | |||
24 | for (const imageInfo of imagesInfo) { | ||
25 | const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width) | ||
23 | 26 | ||
24 | try { | 27 | if (oldImageModel) { |
25 | await oldImageModel.destroy({ transaction: t }) | 28 | // Don't update the avatar if the file URL did not change |
29 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) { | ||
30 | continue | ||
31 | } | ||
26 | 32 | ||
27 | setActorImage(actor, type, null) | 33 | await safeDeleteActorImage(actor, oldImageModel, type, t) |
28 | } catch (err) { | ||
29 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
30 | } | 34 | } |
31 | } | ||
32 | 35 | ||
33 | if (imageInfo) { | ||
34 | const imageModel = await ActorImageModel.create({ | 36 | const imageModel = await ActorImageModel.create({ |
35 | filename: imageInfo.name, | 37 | filename: imageInfo.name, |
36 | onDisk: imageInfo.onDisk ?? false, | 38 | onDisk: imageInfo.onDisk ?? false, |
37 | fileUrl: imageInfo.fileUrl, | 39 | fileUrl: imageInfo.fileUrl, |
38 | height: imageInfo.height, | 40 | height: imageInfo.height, |
39 | width: imageInfo.width, | 41 | width: imageInfo.width, |
40 | type | 42 | type, |
43 | actorId: actor.id | ||
41 | }, { transaction: t }) | 44 | }, { transaction: t }) |
42 | 45 | ||
43 | setActorImage(actor, type, imageModel) | 46 | addActorImage(actor, type, imageModel) |
44 | } | 47 | } |
45 | 48 | ||
46 | return actor | 49 | return actor |
47 | } | 50 | } |
48 | 51 | ||
49 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { | 52 | async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) { |
50 | try { | 53 | try { |
51 | if (type === ActorImageType.AVATAR) { | 54 | const association = buildAssociationName(type) |
52 | await actor.Avatar.destroy({ transaction: t }) | ||
53 | |||
54 | actor.avatarId = null | ||
55 | actor.Avatar = null | ||
56 | } else { | ||
57 | await actor.Banner.destroy({ transaction: t }) | ||
58 | 55 | ||
59 | actor.bannerId = null | 56 | for (const image of actor[association]) { |
60 | actor.Banner = null | 57 | await image.destroy({ transaction: t }) |
61 | } | 58 | } |
59 | |||
60 | actor[association] = [] | ||
62 | } catch (err) { | 61 | } catch (err) { |
63 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) | 62 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) |
64 | } | 63 | } |
@@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy | |||
66 | return actor | 65 | return actor |
67 | } | 66 | } |
68 | 67 | ||
68 | async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) { | ||
69 | try { | ||
70 | await toDelete.destroy({ transaction: t }) | ||
71 | |||
72 | const association = buildAssociationName(type) | ||
73 | actor[association] = actor[association].filter(image => image.id !== toDelete.id) | ||
74 | } catch (err) { | ||
75 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
76 | } | ||
77 | } | ||
78 | |||
69 | // --------------------------------------------------------------------------- | 79 | // --------------------------------------------------------------------------- |
70 | 80 | ||
71 | export { | 81 | export { |
72 | ImageInfo, | 82 | ImageInfo, |
73 | 83 | ||
74 | updateActorImageInstance, | 84 | updateActorImages, |
75 | deleteActorImageInstance | 85 | deleteActorImages |
76 | } | 86 | } |
77 | 87 | ||
78 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
79 | 89 | ||
80 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | 90 | function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) { |
81 | const id = imageModel | 91 | const association = buildAssociationName(type) |
82 | ? imageModel.id | 92 | if (!actor[association]) actor[association] = [] |
83 | : null | 93 | |
84 | 94 | actor[association].push(imageModel) | |
85 | if (type === ActorImageType.AVATAR) { | 95 | } |
86 | actorModel.avatarId = id | ||
87 | actorModel.Avatar = imageModel | ||
88 | } else { | ||
89 | actorModel.bannerId = id | ||
90 | actorModel.Banner = imageModel | ||
91 | } | ||
92 | 96 | ||
93 | return actorModel | 97 | function buildAssociationName (type: ActorImageType) { |
98 | return type === ActorImageType.AVATAR | ||
99 | ? 'Avatars' | ||
100 | : 'Banners' | ||
94 | } | 101 | } |
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts index 999aed97d..500bc9912 100644 --- a/server/lib/activitypub/actors/shared/creator.ts +++ b/server/lib/activitypub/actors/shared/creator.ts | |||
@@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server' | |||
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | 6 | import { VideoChannelModel } from '@server/models/video/video-channel' |
7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' | 7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' |
8 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 8 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
9 | import { updateActorImageInstance } from '../image' | 9 | import { updateActorImages } from '../image' |
10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' | 10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes' |
11 | import { fetchActorFollowsCount } from './url-to-object' | 11 | import { fetchActorFollowsCount } from './url-to-object' |
12 | 12 | ||
13 | export class APActorCreator { | 13 | export class APActorCreator { |
@@ -27,11 +27,11 @@ export class APActorCreator { | |||
27 | return sequelizeTypescript.transaction(async t => { | 27 | return sequelizeTypescript.transaction(async t => { |
28 | const server = await this.setServer(actorInstance, t) | 28 | const server = await this.setServer(actorInstance, t) |
29 | 29 | ||
30 | await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t) | ||
31 | await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t) | ||
32 | |||
33 | const { actorCreated, created } = await this.saveActor(actorInstance, t) | 30 | const { actorCreated, created } = await this.saveActor(actorInstance, t) |
34 | 31 | ||
32 | await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t) | ||
33 | await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t) | ||
34 | |||
35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) | 35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) |
36 | 36 | ||
37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance | 37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance |
@@ -71,10 +71,10 @@ export class APActorCreator { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { | 73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { |
74 | const imageInfo = getImageInfoFromObject(this.actorObject, type) | 74 | const imagesInfo = getImagesInfoFromObject(this.actorObject, type) |
75 | if (!imageInfo) return | 75 | if (imagesInfo.length === 0) return |
76 | 76 | ||
77 | return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) | 77 | return updateActorImages(actor as MActorImages, type, imagesInfo, t) |
78 | } | 78 | } |
79 | 79 | ||
80 | private async saveActor (actor: MActor, t: Transaction) { | 80 | private async saveActor (actor: MActor, t: Transaction) { |
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts index 23bc972e5..f6a78c457 100644 --- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts | |||
@@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor' | |||
4 | import { FilteredModelAttributes } from '@server/types' | 4 | import { FilteredModelAttributes } from '@server/types' |
5 | import { getLowercaseExtension } from '@shared/core-utils' | 5 | import { getLowercaseExtension } from '@shared/core-utils' |
6 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
7 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 7 | import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models' |
8 | 8 | ||
9 | function getActorAttributesFromObject ( | 9 | function getActorAttributesFromObject ( |
10 | actorObject: ActivityPubActor, | 10 | actorObject: ActivityPubActor, |
@@ -30,33 +30,36 @@ function getActorAttributesFromObject ( | |||
30 | } | 30 | } |
31 | } | 31 | } |
32 | 32 | ||
33 | function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { | 33 | function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { |
34 | const mimetypes = MIMETYPES.IMAGE | 34 | const iconsOrImages = type === ActorImageType.AVATAR |
35 | const icon = type === ActorImageType.AVATAR | 35 | ? actorObject.icons || actorObject.icon |
36 | ? actorObject.icon | ||
37 | : actorObject.image | 36 | : actorObject.image |
38 | 37 | ||
39 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | 38 | return normalizeIconOrImage(iconsOrImages).map(iconOrImage => { |
39 | const mimetypes = MIMETYPES.IMAGE | ||
40 | 40 | ||
41 | let extension: string | 41 | if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined |
42 | 42 | ||
43 | if (icon.mediaType) { | 43 | let extension: string |
44 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
45 | } else { | ||
46 | const tmp = getLowercaseExtension(icon.url) | ||
47 | 44 | ||
48 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | 45 | if (iconOrImage.mediaType) { |
49 | } | 46 | extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType] |
47 | } else { | ||
48 | const tmp = getLowercaseExtension(iconOrImage.url) | ||
50 | 49 | ||
51 | if (!extension) return undefined | 50 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp |
51 | } | ||
52 | 52 | ||
53 | return { | 53 | if (!extension) return undefined |
54 | name: buildUUID() + extension, | 54 | |
55 | fileUrl: icon.url, | 55 | return { |
56 | height: icon.height, | 56 | name: buildUUID() + extension, |
57 | width: icon.width, | 57 | fileUrl: iconOrImage.url, |
58 | type | 58 | height: iconOrImage.height, |
59 | } | 59 | width: iconOrImage.width, |
60 | type | ||
61 | } | ||
62 | }) | ||
60 | } | 63 | } |
61 | 64 | ||
62 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | 65 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { |
@@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | |||
65 | 68 | ||
66 | export { | 69 | export { |
67 | getActorAttributesFromObject, | 70 | getActorAttributesFromObject, |
68 | getImageInfoFromObject, | 71 | getImagesInfoFromObject, |
69 | getActorDisplayNameFromObject | 72 | getActorDisplayNameFromObject |
70 | } | 73 | } |
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] { | ||
78 | if (Array.isArray(icon)) return icon | ||
79 | if (icon) return [ icon ] | ||
80 | |||
81 | return [] | ||
82 | } | ||
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts index 042438d9c..fe94af9f1 100644 --- a/server/lib/activitypub/actors/updater.ts +++ b/server/lib/activitypub/actors/updater.ts | |||
@@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel' | |||
5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' | 5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' |
6 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 6 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
7 | import { getOrCreateAPOwner } from './get' | 7 | import { getOrCreateAPOwner } from './get' |
8 | import { updateActorImageInstance } from './image' | 8 | import { updateActorImages } from './image' |
9 | import { fetchActorFollowsCount } from './shared' | 9 | import { fetchActorFollowsCount } from './shared' |
10 | import { getImageInfoFromObject } from './shared/object-to-model-attributes' | 10 | import { getImagesInfoFromObject } from './shared/object-to-model-attributes' |
11 | 11 | ||
12 | export class APActorUpdater { | 12 | export class APActorUpdater { |
13 | 13 | ||
@@ -29,8 +29,8 @@ export class APActorUpdater { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | async update () { | 31 | async update () { |
32 | const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) | 32 | const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR) |
33 | const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) | 33 | const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER) |
34 | 34 | ||
35 | try { | 35 | try { |
36 | await this.updateActorInstance(this.actor, this.actorObject) | 36 | await this.updateActorInstance(this.actor, this.actorObject) |
@@ -47,8 +47,8 @@ export class APActorUpdater { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | await runInReadCommittedTransaction(async t => { | 49 | await runInReadCommittedTransaction(async t => { |
50 | await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) | 50 | await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t) |
51 | await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) | 51 | await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t) |
52 | }) | 52 | }) |
53 | 53 | ||
54 | await runInReadCommittedTransaction(async t => { | 54 | await runInReadCommittedTransaction(async t => { |
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..e9bd148f6 --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import maxBy from 'lodash/maxBy' | ||
2 | |||
3 | function getBiggestActorImage <T extends { width: number }> (images: T[]) { | ||
4 | const image = maxBy(images, 'width') | ||
5 | |||
6 | // If width is null, maxBy won't return a value | ||
7 | if (!image) return images[0] | ||
8 | |||
9 | return image | ||
10 | } | ||
11 | |||
12 | export { | ||
13 | getBiggestActorImage | ||
14 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 19354ab70..c010f3c44 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -3,6 +3,7 @@ import { readFile } from 'fs-extra' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import validator from 'validator' | 4 | import validator from 'validator' |
5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | 5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' |
6 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
6 | import { root } from '@shared/core-utils' | 7 | import { root } from '@shared/core-utils' |
7 | import { escapeHTML } from '@shared/core-utils/renderer' | 8 | import { escapeHTML } from '@shared/core-utils/renderer' |
8 | import { sha256 } from '@shared/extra-utils' | 9 | import { sha256 } from '@shared/extra-utils' |
@@ -16,7 +17,6 @@ import { mdToOneLinePlainText } from '../helpers/markdown' | |||
16 | import { CONFIG } from '../initializers/config' | 17 | import { CONFIG } from '../initializers/config' |
17 | import { | 18 | import { |
18 | ACCEPT_HEADERS, | 19 | ACCEPT_HEADERS, |
19 | ACTOR_IMAGES_SIZE, | ||
20 | CUSTOM_HTML_TAG_COMMENTS, | 20 | CUSTOM_HTML_TAG_COMMENTS, |
21 | EMBED_SIZE, | 21 | EMBED_SIZE, |
22 | FILES_CONTENT_HASH, | 22 | FILES_CONTENT_HASH, |
@@ -29,6 +29,7 @@ import { VideoModel } from '../models/video/video' | |||
29 | import { VideoChannelModel } from '../models/video/video-channel' | 29 | import { VideoChannelModel } from '../models/video/video-channel' |
30 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 30 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
31 | import { MAccountActor, MChannelActor } from '../types/models' | 31 | import { MAccountActor, MChannelActor } from '../types/models' |
32 | import { getBiggestActorImage } from './actor-image' | ||
32 | import { ServerConfigManager } from './server-config-manager' | 33 | import { ServerConfigManager } from './server-config-manager' |
33 | 34 | ||
34 | type Tags = { | 35 | type Tags = { |
@@ -273,10 +274,11 @@ class ClientHtml { | |||
273 | const siteName = CONFIG.INSTANCE.NAME | 274 | const siteName = CONFIG.INSTANCE.NAME |
274 | const title = entity.getDisplayName() | 275 | const title = entity.getDisplayName() |
275 | 276 | ||
277 | const avatar = getBiggestActorImage(entity.Actor.Avatars) | ||
276 | const image = { | 278 | const image = { |
277 | url: entity.Actor.getAvatarUrl(), | 279 | url: ActorImageModel.getImageUrl(avatar), |
278 | width: ACTOR_IMAGES_SIZE.AVATARS.width, | 280 | width: avatar?.width, |
279 | height: ACTOR_IMAGES_SIZE.AVATARS.height | 281 | height: avatar?.height |
280 | } | 282 | } |
281 | 283 | ||
282 | const ogType = 'website' | 284 | const ogType = 'website' |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index c6826759b..01046d017 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | 1 | import { queue } from 'async' |
2 | import { remove } from 'fs-extra' | ||
3 | import LRUCache from 'lru-cache' | 3 | import LRUCache from 'lru-cache' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../initializers/config' | |||
13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' | 13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' |
14 | import { sequelizeTypescript } from '../initializers/database' | 14 | import { sequelizeTypescript } from '../initializers/database' |
15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
16 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors' | 16 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
17 | import { sendUpdateActor } from './activitypub/send' | 17 | import { sendUpdateActor } from './activitypub/send' |
18 | 18 | ||
19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
@@ -33,64 +33,69 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
33 | }) as MActor | 33 | }) as MActor |
34 | } | 34 | } |
35 | 35 | ||
36 | async function updateLocalActorImageFile ( | 36 | async function updateLocalActorImageFiles ( |
37 | accountOrChannel: MAccountDefault | MChannelDefault, | 37 | accountOrChannel: MAccountDefault | MChannelDefault, |
38 | imagePhysicalFile: Express.Multer.File, | 38 | imagePhysicalFile: Express.Multer.File, |
39 | type: ActorImageType | 39 | type: ActorImageType |
40 | ) { | 40 | ) { |
41 | const imageSize = type === ActorImageType.AVATAR | 41 | const processImageSize = async (imageSize: { width: number, height: number }) => { |
42 | ? ACTOR_IMAGES_SIZE.AVATARS | 42 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
43 | : ACTOR_IMAGES_SIZE.BANNERS | 43 | |
44 | 44 | const imageName = buildUUID() + extension | |
45 | const extension = getLowercaseExtension(imagePhysicalFile.filename) | 45 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) |
46 | 46 | await processImage(imagePhysicalFile.path, destination, imageSize, true) | |
47 | const imageName = buildUUID() + extension | 47 | |
48 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 48 | return { |
49 | await processImage(imagePhysicalFile.path, destination, imageSize) | 49 | imageName, |
50 | 50 | imageSize | |
51 | return retryTransactionWrapper(() => { | 51 | } |
52 | return sequelizeTypescript.transaction(async t => { | 52 | } |
53 | const actorImageInfo = { | 53 | |
54 | name: imageName, | 54 | const processedImages = await Promise.all(ACTOR_IMAGES_SIZE[type].map(processImageSize)) |
55 | fileUrl: null, | 55 | await remove(imagePhysicalFile.path) |
56 | height: imageSize.height, | 56 | |
57 | width: imageSize.width, | 57 | return retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => { |
58 | onDisk: true | 58 | const actorImagesInfo = processedImages.map(({ imageName, imageSize }) => ({ |
59 | } | 59 | name: imageName, |
60 | 60 | fileUrl: null, | |
61 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | 61 | height: imageSize.height, |
62 | await updatedActor.save({ transaction: t }) | 62 | width: imageSize.width, |
63 | 63 | onDisk: true | |
64 | await sendUpdateActor(accountOrChannel, t) | 64 | })) |
65 | 65 | ||
66 | return type === ActorImageType.AVATAR | 66 | const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t) |
67 | ? updatedActor.Avatar | 67 | await updatedActor.save({ transaction: t }) |
68 | : updatedActor.Banner | 68 | |
69 | }) | 69 | await sendUpdateActor(accountOrChannel, t) |
70 | }) | 70 | |
71 | return type === ActorImageType.AVATAR | ||
72 | ? updatedActor.Avatars | ||
73 | : updatedActor.Banners | ||
74 | })) | ||
71 | } | 75 | } |
72 | 76 | ||
73 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | 77 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { |
74 | return retryTransactionWrapper(() => { | 78 | return retryTransactionWrapper(() => { |
75 | return sequelizeTypescript.transaction(async t => { | 79 | return sequelizeTypescript.transaction(async t => { |
76 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | 80 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) |
77 | await updatedActor.save({ transaction: t }) | 81 | await updatedActor.save({ transaction: t }) |
78 | 82 | ||
79 | await sendUpdateActor(accountOrChannel, t) | 83 | await sendUpdateActor(accountOrChannel, t) |
80 | 84 | ||
81 | return updatedActor.Avatar | 85 | return updatedActor.Avatars |
82 | }) | 86 | }) |
83 | }) | 87 | }) |
84 | } | 88 | } |
85 | 89 | ||
86 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | 90 | type DownloadImageQueueTask = { |
91 | fileUrl: string | ||
92 | filename: string | ||
93 | type: ActorImageType | ||
94 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | ||
95 | } | ||
87 | 96 | ||
88 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 97 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { |
89 | const size = task.type === ActorImageType.AVATAR | 98 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size) |
90 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
91 | : ACTOR_IMAGES_SIZE.BANNERS | ||
92 | |||
93 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
94 | .then(() => cb()) | 99 | .then(() => cb()) |
95 | .catch(err => cb(err)) | 100 | .catch(err => cb(err)) |
96 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | 101 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) |
@@ -110,7 +115,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE. | |||
110 | 115 | ||
111 | export { | 116 | export { |
112 | actorImagePathUnsafeCache, | 117 | actorImagePathUnsafeCache, |
113 | updateLocalActorImageFile, | 118 | updateLocalActorImageFiles, |
114 | deleteLocalActorImageFile, | 119 | deleteLocalActorImageFile, |
115 | pushActorImageProcessInQueue, | 120 | pushActorImageProcessInQueue, |
116 | buildActorInstance | 121 | buildActorInstance |
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts index 765cbaad9..ecd1687b4 100644 --- a/server/lib/notifier/shared/comment/comment-mention.ts +++ b/server/lib/notifier/shared/comment/comment-mention.ts | |||
@@ -77,7 +77,7 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU | |||
77 | userId: user.id, | 77 | userId: user.id, |
78 | commentId: this.payload.id | 78 | commentId: this.payload.id |
79 | }) | 79 | }) |
80 | notification.Comment = this.payload | 80 | notification.VideoComment = this.payload |
81 | 81 | ||
82 | return notification | 82 | return notification |
83 | } | 83 | } |
diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts index b76fc15bf..757502703 100644 --- a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts +++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts | |||
@@ -44,7 +44,7 @@ export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwner | |||
44 | userId: user.id, | 44 | userId: user.id, |
45 | commentId: this.payload.id | 45 | commentId: this.payload.id |
46 | }) | 46 | }) |
47 | notification.Comment = this.payload | 47 | notification.VideoComment = this.payload |
48 | 48 | ||
49 | return notification | 49 | return notification |
50 | } | 50 | } |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 6a441a210..d9eb25f0f 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | 2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' |
3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | 3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { AbuseMessage } from '@shared/models' | 4 | import { AbuseMessage } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
7 | import { getSort, throwIfNotValid } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
8 | import { AbuseModel } from './abuse' | 8 | import { AbuseModel } from './abuse' |
9 | import { FindOptions } from 'sequelize/dist' | ||
9 | 10 | ||
10 | @Table({ | 11 | @Table({ |
11 | tableName: 'abuseMessage', | 12 | tableName: 'abuseMessage', |
@@ -62,21 +63,28 @@ export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessage | |||
62 | Abuse: AbuseModel | 63 | Abuse: AbuseModel |
63 | 64 | ||
64 | static listForApi (abuseId: number) { | 65 | static listForApi (abuseId: number) { |
65 | const options = { | 66 | const getQuery = (forCount: boolean) => { |
66 | where: { abuseId }, | 67 | const query: FindOptions = { |
68 | where: { abuseId }, | ||
69 | order: getSort('createdAt') | ||
70 | } | ||
67 | 71 | ||
68 | order: getSort('createdAt'), | 72 | if (forCount !== true) { |
73 | query.include = [ | ||
74 | { | ||
75 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
76 | required: false | ||
77 | } | ||
78 | ] | ||
79 | } | ||
69 | 80 | ||
70 | include: [ | 81 | return query |
71 | { | ||
72 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
73 | required: false | ||
74 | } | ||
75 | ] | ||
76 | } | 82 | } |
77 | 83 | ||
78 | return AbuseMessageModel.findAndCountAll(options) | 84 | return Promise.all([ |
79 | .then(({ rows, count }) => ({ data: rows, total: count })) | 85 | AbuseMessageModel.count(getQuery(true)), |
86 | AbuseMessageModel.findAll(getQuery(false)) | ||
87 | ]).then(([ total, data ]) => ({ total, data })) | ||
80 | } | 88 | } |
81 | 89 | ||
82 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { | 90 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 1162962bf..a7b8db076 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { FindOptions, Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | 3 | import { handlesToNameAndHost } from '@server/helpers/actors' |
4 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 4 | import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
@@ -9,27 +9,6 @@ import { ServerModel } from '../server/server' | |||
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | enum ScopeNames { | ||
13 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | ||
14 | } | ||
15 | |||
16 | @Scopes(() => ({ | ||
17 | [ScopeNames.WITH_ACCOUNTS]: { | ||
18 | include: [ | ||
19 | { | ||
20 | model: AccountModel, | ||
21 | required: true, | ||
22 | as: 'ByAccount' | ||
23 | }, | ||
24 | { | ||
25 | model: AccountModel, | ||
26 | required: true, | ||
27 | as: 'BlockedAccount' | ||
28 | } | ||
29 | ] | ||
30 | } | ||
31 | })) | ||
32 | |||
33 | @Table({ | 12 | @Table({ |
34 | tableName: 'accountBlocklist', | 13 | tableName: 'accountBlocklist', |
35 | indexes: [ | 14 | indexes: [ |
@@ -123,33 +102,45 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB | |||
123 | }) { | 102 | }) { |
124 | const { start, count, sort, search, accountId } = parameters | 103 | const { start, count, sort, search, accountId } = parameters |
125 | 104 | ||
126 | const query = { | 105 | const getQuery = (forCount: boolean) => { |
127 | offset: start, | 106 | const query: FindOptions = { |
128 | limit: count, | 107 | offset: start, |
129 | order: getSort(sort) | 108 | limit: count, |
130 | } | 109 | order: getSort(sort), |
110 | where: { accountId } | ||
111 | } | ||
131 | 112 | ||
132 | const where = { | 113 | if (search) { |
133 | accountId | 114 | Object.assign(query.where, { |
134 | } | 115 | [Op.or]: [ |
116 | searchAttribute(search, '$BlockedAccount.name$'), | ||
117 | searchAttribute(search, '$BlockedAccount.Actor.url$') | ||
118 | ] | ||
119 | }) | ||
120 | } | ||
135 | 121 | ||
136 | if (search) { | 122 | if (forCount !== true) { |
137 | Object.assign(where, { | 123 | query.include = [ |
138 | [Op.or]: [ | 124 | { |
139 | searchAttribute(search, '$BlockedAccount.name$'), | 125 | model: AccountModel, |
140 | searchAttribute(search, '$BlockedAccount.Actor.url$') | 126 | required: true, |
127 | as: 'ByAccount' | ||
128 | }, | ||
129 | { | ||
130 | model: AccountModel, | ||
131 | required: true, | ||
132 | as: 'BlockedAccount' | ||
133 | } | ||
141 | ] | 134 | ] |
142 | }) | 135 | } |
143 | } | ||
144 | 136 | ||
145 | Object.assign(query, { where }) | 137 | return query |
138 | } | ||
146 | 139 | ||
147 | return AccountBlocklistModel | 140 | return Promise.all([ |
148 | .scope([ ScopeNames.WITH_ACCOUNTS ]) | 141 | AccountBlocklistModel.count(getQuery(true)), |
149 | .findAndCountAll<MAccountBlocklistAccounts>(query) | 142 | AccountBlocklistModel.findAll(getQuery(false)) |
150 | .then(({ rows, count }) => { | 143 | ]).then(([ total, data ]) => ({ total, data })) |
151 | return { total: count, data: rows } | ||
152 | }) | ||
153 | } | 144 | } |
154 | 145 | ||
155 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { | 146 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index e89d31adf..7303651eb 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -121,29 +121,40 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
121 | type?: string | 121 | type?: string |
122 | accountId: number | 122 | accountId: number |
123 | }) { | 123 | }) { |
124 | const query: FindOptions = { | 124 | const getQuery = (forCount: boolean) => { |
125 | offset: options.start, | 125 | const query: FindOptions = { |
126 | limit: options.count, | 126 | offset: options.start, |
127 | order: getSort(options.sort), | 127 | limit: options.count, |
128 | where: { | 128 | order: getSort(options.sort), |
129 | accountId: options.accountId | 129 | where: { |
130 | }, | 130 | accountId: options.accountId |
131 | include: [ | ||
132 | { | ||
133 | model: VideoModel, | ||
134 | required: true, | ||
135 | include: [ | ||
136 | { | ||
137 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
138 | required: true | ||
139 | } | ||
140 | ] | ||
141 | } | 131 | } |
142 | ] | 132 | } |
133 | |||
134 | if (options.type) query.where['type'] = options.type | ||
135 | |||
136 | if (forCount !== true) { | ||
137 | query.include = [ | ||
138 | { | ||
139 | model: VideoModel, | ||
140 | required: true, | ||
141 | include: [ | ||
142 | { | ||
143 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
144 | required: true | ||
145 | } | ||
146 | ] | ||
147 | } | ||
148 | ] | ||
149 | } | ||
150 | |||
151 | return query | ||
143 | } | 152 | } |
144 | if (options.type) query.where['type'] = options.type | ||
145 | 153 | ||
146 | return AccountVideoRateModel.findAndCountAll(query) | 154 | return Promise.all([ |
155 | AccountVideoRateModel.count(getQuery(true)), | ||
156 | AccountVideoRateModel.findAll(getQuery(false)) | ||
157 | ]).then(([ total, data ]) => ({ total, data })) | ||
147 | } | 158 | } |
148 | 159 | ||
149 | static listRemoteRateUrlsOfLocalVideos () { | 160 | static listRemoteRateUrlsOfLocalVideos () { |
@@ -232,7 +243,10 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
232 | ] | 243 | ] |
233 | } | 244 | } |
234 | 245 | ||
235 | return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query) | 246 | return Promise.all([ |
247 | AccountVideoRateModel.count(query), | ||
248 | AccountVideoRateModel.findAll<MAccountVideoRateAccountUrl>(query) | ||
249 | ]).then(([ total, data ]) => ({ total, data })) | ||
236 | } | 250 | } |
237 | 251 | ||
238 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { | 252 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 619a598dd..8a7dfba94 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -54,6 +54,7 @@ export type SummaryOptions = { | |||
54 | whereActor?: WhereOptions | 54 | whereActor?: WhereOptions |
55 | whereServer?: WhereOptions | 55 | whereServer?: WhereOptions |
56 | withAccountBlockerIds?: number[] | 56 | withAccountBlockerIds?: number[] |
57 | forCount?: boolean | ||
57 | } | 58 | } |
58 | 59 | ||
59 | @DefaultScope(() => ({ | 60 | @DefaultScope(() => ({ |
@@ -73,22 +74,24 @@ export type SummaryOptions = { | |||
73 | where: options.whereServer | 74 | where: options.whereServer |
74 | } | 75 | } |
75 | 76 | ||
76 | const queryInclude: Includeable[] = [ | 77 | const actorInclude: Includeable = { |
77 | { | 78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 79 | model: ActorModel.unscoped(), |
79 | model: ActorModel.unscoped(), | 80 | required: options.actorRequired ?? true, |
80 | required: options.actorRequired ?? true, | 81 | where: options.whereActor, |
81 | where: options.whereActor, | 82 | include: [ serverInclude ] |
82 | include: [ | 83 | } |
83 | serverInclude, | ||
84 | 84 | ||
85 | { | 85 | if (options.forCount !== true) { |
86 | model: ActorImageModel.unscoped(), | 86 | actorInclude.include.push({ |
87 | as: 'Avatar', | 87 | model: ActorImageModel, |
88 | required: false | 88 | as: 'Avatars', |
89 | } | 89 | required: false |
90 | ] | 90 | }) |
91 | } | 91 | } |
92 | |||
93 | const queryInclude: Includeable[] = [ | ||
94 | actorInclude | ||
92 | ] | 95 | ] |
93 | 96 | ||
94 | const query: FindOptions = { | 97 | const query: FindOptions = { |
@@ -349,13 +352,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
349 | order: getSort(sort) | 352 | order: getSort(sort) |
350 | } | 353 | } |
351 | 354 | ||
352 | return AccountModel.findAndCountAll(query) | 355 | return Promise.all([ |
353 | .then(({ rows, count }) => { | 356 | AccountModel.count(), |
354 | return { | 357 | AccountModel.findAll(query) |
355 | data: rows, | 358 | ]).then(([ total, data ]) => ({ total, data })) |
356 | total: count | ||
357 | } | ||
358 | }) | ||
359 | } | 359 | } |
360 | 360 | ||
361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { | 361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { |
@@ -407,16 +407,15 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
407 | } | 407 | } |
408 | 408 | ||
409 | toFormattedJSON (this: MAccountFormattable): Account { | 409 | toFormattedJSON (this: MAccountFormattable): Account { |
410 | const actor = this.Actor.toFormattedJSON() | 410 | return { |
411 | const account = { | 411 | ...this.Actor.toFormattedJSON(), |
412 | |||
412 | id: this.id, | 413 | id: this.id, |
413 | displayName: this.getDisplayName(), | 414 | displayName: this.getDisplayName(), |
414 | description: this.description, | 415 | description: this.description, |
415 | updatedAt: this.updatedAt, | 416 | updatedAt: this.updatedAt, |
416 | userId: this.userId ? this.userId : undefined | 417 | userId: this.userId ?? undefined |
417 | } | 418 | } |
418 | |||
419 | return Object.assign(actor, account) | ||
420 | } | 419 | } |
421 | 420 | ||
422 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { | 421 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { |
@@ -424,10 +423,14 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
424 | 423 | ||
425 | return { | 424 | return { |
426 | id: this.id, | 425 | id: this.id, |
427 | name: actor.name, | ||
428 | displayName: this.getDisplayName(), | 426 | displayName: this.getDisplayName(), |
427 | |||
428 | name: actor.name, | ||
429 | url: actor.url, | 429 | url: actor.url, |
430 | host: actor.host, | 430 | host: actor.host, |
431 | avatars: actor.avatars, | ||
432 | |||
433 | // TODO: remove, deprecated in 4.2 | ||
431 | avatar: actor.avatar | 434 | avatar: actor.avatar |
432 | } | 435 | } |
433 | } | 436 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 006282530..0f4d3c0a6 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { difference, values } from 'lodash' | 1 | import { difference, values } from 'lodash' |
2 | import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' | 2 | import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AfterCreate, | 4 | AfterCreate, |
5 | AfterDestroy, | 5 | AfterDestroy, |
@@ -30,12 +30,12 @@ import { | |||
30 | MActorFollowFormattable, | 30 | MActorFollowFormattable, |
31 | MActorFollowSubscriptions | 31 | MActorFollowSubscriptions |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { AttributesOnly } from '@shared/typescript-utils' | ||
34 | import { ActivityPubActorType } from '@shared/models' | 33 | import { ActivityPubActorType } from '@shared/models' |
34 | import { AttributesOnly } from '@shared/typescript-utils' | ||
35 | import { FollowState } from '../../../shared/models/actors' | 35 | import { FollowState } from '../../../shared/models/actors' |
36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
37 | import { logger } from '../../helpers/logger' | 37 | import { logger } from '../../helpers/logger' |
38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants' |
39 | import { AccountModel } from '../account/account' | 39 | import { AccountModel } from '../account/account' |
40 | import { ServerModel } from '../server/server' | 40 | import { ServerModel } from '../server/server' |
41 | import { doesExist } from '../shared/query' | 41 | import { doesExist } from '../shared/query' |
@@ -375,43 +375,46 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
375 | Object.assign(followingWhere, { type: actorType }) | 375 | Object.assign(followingWhere, { type: actorType }) |
376 | } | 376 | } |
377 | 377 | ||
378 | const query = { | 378 | const getQuery = (forCount: boolean) => { |
379 | distinct: true, | 379 | const actorModel = forCount |
380 | offset: start, | 380 | ? ActorModel.unscoped() |
381 | limit: count, | 381 | : ActorModel |
382 | order: getFollowsSort(sort), | 382 | |
383 | where: followWhere, | 383 | return { |
384 | include: [ | 384 | distinct: true, |
385 | { | 385 | offset: start, |
386 | model: ActorModel, | 386 | limit: count, |
387 | required: true, | 387 | order: getFollowsSort(sort), |
388 | as: 'ActorFollower', | 388 | where: followWhere, |
389 | where: { | 389 | include: [ |
390 | id | 390 | { |
391 | } | 391 | model: actorModel, |
392 | }, | 392 | required: true, |
393 | { | 393 | as: 'ActorFollower', |
394 | model: ActorModel, | 394 | where: { |
395 | as: 'ActorFollowing', | 395 | id |
396 | required: true, | ||
397 | where: followingWhere, | ||
398 | include: [ | ||
399 | { | ||
400 | model: ServerModel, | ||
401 | required: true | ||
402 | } | 396 | } |
403 | ] | 397 | }, |
404 | } | 398 | { |
405 | ] | 399 | model: actorModel, |
400 | as: 'ActorFollowing', | ||
401 | required: true, | ||
402 | where: followingWhere, | ||
403 | include: [ | ||
404 | { | ||
405 | model: ServerModel, | ||
406 | required: true | ||
407 | } | ||
408 | ] | ||
409 | } | ||
410 | ] | ||
411 | } | ||
406 | } | 412 | } |
407 | 413 | ||
408 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 414 | return Promise.all([ |
409 | .then(({ rows, count }) => { | 415 | ActorFollowModel.count(getQuery(true)), |
410 | return { | 416 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
411 | data: rows, | 417 | ]).then(([ total, data ]) => ({ total, data })) |
412 | total: count | ||
413 | } | ||
414 | }) | ||
415 | } | 418 | } |
416 | 419 | ||
417 | static listFollowersForApi (options: { | 420 | static listFollowersForApi (options: { |
@@ -429,11 +432,17 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
429 | const followerWhere: WhereOptions = {} | 432 | const followerWhere: WhereOptions = {} |
430 | 433 | ||
431 | if (search) { | 434 | if (search) { |
432 | Object.assign(followWhere, { | 435 | const escapedSearch = ActorFollowModel.sequelize.escape('%' + search + '%') |
433 | [Op.or]: [ | 436 | |
434 | searchAttribute(search, '$ActorFollower.preferredUsername$'), | 437 | Object.assign(followerWhere, { |
435 | searchAttribute(search, '$ActorFollower.Server.host$') | 438 | id: { |
436 | ] | 439 | [Op.in]: literal( |
440 | `(` + | ||
441 | `SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" ` + | ||
442 | `WHERE "preferredUsername" ILIKE ${escapedSearch} OR "host" ILIKE ${escapedSearch}` + | ||
443 | `)` | ||
444 | ) | ||
445 | } | ||
437 | }) | 446 | }) |
438 | } | 447 | } |
439 | 448 | ||
@@ -441,39 +450,43 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
441 | Object.assign(followerWhere, { type: actorType }) | 450 | Object.assign(followerWhere, { type: actorType }) |
442 | } | 451 | } |
443 | 452 | ||
444 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
445 | distinct: true, | 454 | const actorModel = forCount |
446 | offset: start, | 455 | ? ActorModel.unscoped() |
447 | limit: count, | 456 | : ActorModel |
448 | order: getFollowsSort(sort), | 457 | |
449 | where: followWhere, | 458 | return { |
450 | include: [ | 459 | distinct: true, |
451 | { | 460 | |
452 | model: ActorModel, | 461 | offset: start, |
453 | required: true, | 462 | limit: count, |
454 | as: 'ActorFollower', | 463 | order: getFollowsSort(sort), |
455 | where: followerWhere | 464 | where: followWhere, |
456 | }, | 465 | include: [ |
457 | { | 466 | { |
458 | model: ActorModel, | 467 | model: actorModel, |
459 | as: 'ActorFollowing', | 468 | required: true, |
460 | required: true, | 469 | as: 'ActorFollower', |
461 | where: { | 470 | where: followerWhere |
462 | id: { | 471 | }, |
463 | [Op.in]: actorIds | 472 | { |
473 | model: actorModel, | ||
474 | as: 'ActorFollowing', | ||
475 | required: true, | ||
476 | where: { | ||
477 | id: { | ||
478 | [Op.in]: actorIds | ||
479 | } | ||
464 | } | 480 | } |
465 | } | 481 | } |
466 | } | 482 | ] |
467 | ] | 483 | } |
468 | } | 484 | } |
469 | 485 | ||
470 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 486 | return Promise.all([ |
471 | .then(({ rows, count }) => { | 487 | ActorFollowModel.count(getQuery(true)), |
472 | return { | 488 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
473 | data: rows, | 489 | ]).then(([ total, data ]) => ({ total, data })) |
474 | total: count | ||
475 | } | ||
476 | }) | ||
477 | } | 490 | } |
478 | 491 | ||
479 | static listSubscriptionsForApi (options: { | 492 | static listSubscriptionsForApi (options: { |
@@ -497,58 +510,68 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
497 | }) | 510 | }) |
498 | } | 511 | } |
499 | 512 | ||
500 | const query = { | 513 | const getQuery = (forCount: boolean) => { |
501 | attributes: [], | 514 | let channelInclude: Includeable[] = [] |
502 | distinct: true, | 515 | |
503 | offset: start, | 516 | if (forCount !== true) { |
504 | limit: count, | 517 | channelInclude = [ |
505 | order: getSort(sort), | 518 | { |
506 | where, | 519 | attributes: { |
507 | include: [ | 520 | exclude: unusedActorAttributesForAPI |
508 | { | 521 | }, |
509 | attributes: [ 'id' ], | 522 | model: ActorModel, |
510 | model: ActorModel.unscoped(), | 523 | required: true |
511 | as: 'ActorFollowing', | 524 | }, |
512 | required: true, | 525 | { |
513 | include: [ | 526 | model: AccountModel.unscoped(), |
514 | { | 527 | required: true, |
515 | model: VideoChannelModel.unscoped(), | 528 | include: [ |
516 | required: true, | 529 | { |
517 | include: [ | 530 | attributes: { |
518 | { | 531 | exclude: unusedActorAttributesForAPI |
519 | attributes: { | ||
520 | exclude: unusedActorAttributesForAPI | ||
521 | }, | ||
522 | model: ActorModel, | ||
523 | required: true | ||
524 | }, | 532 | }, |
525 | { | 533 | model: ActorModel, |
526 | model: AccountModel.unscoped(), | 534 | required: true |
527 | required: true, | 535 | } |
528 | include: [ | 536 | ] |
529 | { | 537 | } |
530 | attributes: { | 538 | ] |
531 | exclude: unusedActorAttributesForAPI | 539 | } |
532 | }, | 540 | |
533 | model: ActorModel, | 541 | return { |
534 | required: true | 542 | attributes: forCount === true |
535 | } | 543 | ? [] |
536 | ] | 544 | : SORTABLE_COLUMNS.USER_SUBSCRIPTIONS, |
537 | } | 545 | distinct: true, |
538 | ] | 546 | offset: start, |
539 | } | 547 | limit: count, |
540 | ] | 548 | order: getSort(sort), |
541 | } | 549 | where, |
542 | ] | 550 | include: [ |
551 | { | ||
552 | attributes: [ 'id' ], | ||
553 | model: ActorModel.unscoped(), | ||
554 | as: 'ActorFollowing', | ||
555 | required: true, | ||
556 | include: [ | ||
557 | { | ||
558 | model: VideoChannelModel.unscoped(), | ||
559 | required: true, | ||
560 | include: channelInclude | ||
561 | } | ||
562 | ] | ||
563 | } | ||
564 | ] | ||
565 | } | ||
543 | } | 566 | } |
544 | 567 | ||
545 | return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query) | 568 | return Promise.all([ |
546 | .then(({ rows, count }) => { | 569 | ActorFollowModel.count(getQuery(true)), |
547 | return { | 570 | ActorFollowModel.findAll<MActorFollowSubscriptions>(getQuery(false)) |
548 | data: rows.map(r => r.ActorFollowing.VideoChannel), | 571 | ]).then(([ total, rows ]) => ({ |
549 | total: count | 572 | total, |
550 | } | 573 | data: rows.map(r => r.ActorFollowing.VideoChannel) |
551 | }) | 574 | })) |
552 | } | 575 | } |
553 | 576 | ||
554 | static async keepUnfollowedInstance (hosts: string[]) { | 577 | static async keepUnfollowedInstance (hosts: string[]) { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 8edff5ab4..f74ab735e 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -1,15 +1,29 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { |
4 | import { MActorImageFormattable } from '@server/types/models' | 4 | AfterDestroy, |
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { MActorImage, MActorImageFormattable } from '@server/types/models' | ||
17 | import { getLowercaseExtension } from '@shared/core-utils' | ||
18 | import { ActivityIconObject, ActorImageType } from '@shared/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 20 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
9 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
11 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
12 | import { throwIfNotValid } from '../utils' | 25 | import { throwIfNotValid } from '../utils' |
26 | import { ActorModel } from './actor' | ||
13 | 27 | ||
14 | @Table({ | 28 | @Table({ |
15 | tableName: 'actorImage', | 29 | tableName: 'actorImage', |
@@ -17,6 +31,10 @@ import { throwIfNotValid } from '../utils' | |||
17 | { | 31 | { |
18 | fields: [ 'filename' ], | 32 | fields: [ 'filename' ], |
19 | unique: true | 33 | unique: true |
34 | }, | ||
35 | { | ||
36 | fields: [ 'actorId', 'type', 'width' ], | ||
37 | unique: true | ||
20 | } | 38 | } |
21 | ] | 39 | ] |
22 | }) | 40 | }) |
@@ -55,6 +73,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
55 | @UpdatedAt | 73 | @UpdatedAt |
56 | updatedAt: Date | 74 | updatedAt: Date |
57 | 75 | ||
76 | @ForeignKey(() => ActorModel) | ||
77 | @Column | ||
78 | actorId: number | ||
79 | |||
80 | @BelongsTo(() => ActorModel, { | ||
81 | foreignKey: { | ||
82 | allowNull: false | ||
83 | }, | ||
84 | onDelete: 'CASCADE' | ||
85 | }) | ||
86 | Actor: ActorModel | ||
87 | |||
58 | @AfterDestroy | 88 | @AfterDestroy |
59 | static removeFilesAndSendDelete (instance: ActorImageModel) { | 89 | static removeFilesAndSendDelete (instance: ActorImageModel) { |
60 | logger.info('Removing actor image file %s.', instance.filename) | 90 | logger.info('Removing actor image file %s.', instance.filename) |
@@ -74,20 +104,41 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
74 | return ActorImageModel.findOne(query) | 104 | return ActorImageModel.findOne(query) |
75 | } | 105 | } |
76 | 106 | ||
107 | static getImageUrl (image: MActorImage) { | ||
108 | if (!image) return undefined | ||
109 | |||
110 | return WEBSERVER.URL + image.getStaticPath() | ||
111 | } | ||
112 | |||
77 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | 113 | toFormattedJSON (this: MActorImageFormattable): ActorImage { |
78 | return { | 114 | return { |
115 | width: this.width, | ||
79 | path: this.getStaticPath(), | 116 | path: this.getStaticPath(), |
80 | createdAt: this.createdAt, | 117 | createdAt: this.createdAt, |
81 | updatedAt: this.updatedAt | 118 | updatedAt: this.updatedAt |
82 | } | 119 | } |
83 | } | 120 | } |
84 | 121 | ||
85 | getStaticPath () { | 122 | toActivityPubObject (): ActivityIconObject { |
86 | if (this.type === ActorImageType.AVATAR) { | 123 | const extension = getLowercaseExtension(this.filename) |
87 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | 124 | |
125 | return { | ||
126 | type: 'Image', | ||
127 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
128 | height: this.height, | ||
129 | width: this.width, | ||
130 | url: ActorImageModel.getImageUrl(this) | ||
88 | } | 131 | } |
132 | } | ||
89 | 133 | ||
90 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | 134 | getStaticPath () { |
135 | switch (this.type) { | ||
136 | case ActorImageType.AVATAR: | ||
137 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
138 | |||
139 | case ActorImageType.BANNER: | ||
140 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
141 | } | ||
91 | } | 142 | } |
92 | 143 | ||
93 | getPath () { | 144 | getPath () { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index c12dcf634..08cb2fd24 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -16,11 +16,11 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
19 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
20 | import { getLowercaseExtension } from '@shared/core-utils' | 21 | import { getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | ||
23 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
24 | import { activityPubContextify } from '../../helpers/activitypub' | 24 | import { activityPubContextify } from '../../helpers/activitypub' |
25 | import { | 25 | import { |
26 | isActorFollowersCountValid, | 26 | isActorFollowersCountValid, |
@@ -81,7 +81,7 @@ export const unusedActorAttributesForAPI = [ | |||
81 | }, | 81 | }, |
82 | { | 82 | { |
83 | model: ActorImageModel, | 83 | model: ActorImageModel, |
84 | as: 'Avatar', | 84 | as: 'Avatars', |
85 | required: false | 85 | required: false |
86 | } | 86 | } |
87 | ] | 87 | ] |
@@ -109,12 +109,12 @@ export const unusedActorAttributesForAPI = [ | |||
109 | }, | 109 | }, |
110 | { | 110 | { |
111 | model: ActorImageModel, | 111 | model: ActorImageModel, |
112 | as: 'Avatar', | 112 | as: 'Avatars', |
113 | required: false | 113 | required: false |
114 | }, | 114 | }, |
115 | { | 115 | { |
116 | model: ActorImageModel, | 116 | model: ActorImageModel, |
117 | as: 'Banner', | 117 | as: 'Banners', |
118 | required: false | 118 | required: false |
119 | } | 119 | } |
120 | ] | 120 | ] |
@@ -153,9 +153,6 @@ export const unusedActorAttributesForAPI = [ | |||
153 | fields: [ 'serverId' ] | 153 | fields: [ 'serverId' ] |
154 | }, | 154 | }, |
155 | { | 155 | { |
156 | fields: [ 'avatarId' ] | ||
157 | }, | ||
158 | { | ||
159 | fields: [ 'followersUrl' ] | 156 | fields: [ 'followersUrl' ] |
160 | } | 157 | } |
161 | ] | 158 | ] |
@@ -231,35 +228,31 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
231 | @UpdatedAt | 228 | @UpdatedAt |
232 | updatedAt: Date | 229 | updatedAt: Date |
233 | 230 | ||
234 | @ForeignKey(() => ActorImageModel) | 231 | @HasMany(() => ActorImageModel, { |
235 | @Column | 232 | as: 'Avatars', |
236 | avatarId: number | 233 | onDelete: 'cascade', |
237 | 234 | hooks: true, | |
238 | @ForeignKey(() => ActorImageModel) | ||
239 | @Column | ||
240 | bannerId: number | ||
241 | |||
242 | @BelongsTo(() => ActorImageModel, { | ||
243 | foreignKey: { | 235 | foreignKey: { |
244 | name: 'avatarId', | 236 | allowNull: false |
245 | allowNull: true | ||
246 | }, | 237 | }, |
247 | as: 'Avatar', | 238 | scope: { |
248 | onDelete: 'set null', | 239 | type: ActorImageType.AVATAR |
249 | hooks: true | 240 | } |
250 | }) | 241 | }) |
251 | Avatar: ActorImageModel | 242 | Avatars: ActorImageModel[] |
252 | 243 | ||
253 | @BelongsTo(() => ActorImageModel, { | 244 | @HasMany(() => ActorImageModel, { |
245 | as: 'Banners', | ||
246 | onDelete: 'cascade', | ||
247 | hooks: true, | ||
254 | foreignKey: { | 248 | foreignKey: { |
255 | name: 'bannerId', | 249 | allowNull: false |
256 | allowNull: true | ||
257 | }, | 250 | }, |
258 | as: 'Banner', | 251 | scope: { |
259 | onDelete: 'set null', | 252 | type: ActorImageType.BANNER |
260 | hooks: true | 253 | } |
261 | }) | 254 | }) |
262 | Banner: ActorImageModel | 255 | Banners: ActorImageModel[] |
263 | 256 | ||
264 | @HasMany(() => ActorFollowModel, { | 257 | @HasMany(() => ActorFollowModel, { |
265 | foreignKey: { | 258 | foreignKey: { |
@@ -386,8 +379,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
386 | transaction | 379 | transaction |
387 | } | 380 | } |
388 | 381 | ||
389 | return ActorModel.scope(ScopeNames.FULL) | 382 | return ActorModel.scope(ScopeNames.FULL).findOne(query) |
390 | .findOne(query) | ||
391 | } | 383 | } |
392 | 384 | ||
393 | return ModelCache.Instance.doCache({ | 385 | return ModelCache.Instance.doCache({ |
@@ -410,8 +402,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
410 | transaction | 402 | transaction |
411 | } | 403 | } |
412 | 404 | ||
413 | return ActorModel.unscoped() | 405 | return ActorModel.unscoped().findOne(query) |
414 | .findOne(query) | ||
415 | } | 406 | } |
416 | 407 | ||
417 | return ModelCache.Instance.doCache({ | 408 | return ModelCache.Instance.doCache({ |
@@ -532,55 +523,50 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
532 | } | 523 | } |
533 | 524 | ||
534 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 525 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
535 | let avatar: ActorImage = null | ||
536 | if (this.Avatar) { | ||
537 | avatar = this.Avatar.toFormattedJSON() | ||
538 | } | ||
539 | |||
540 | return { | 526 | return { |
541 | url: this.url, | 527 | url: this.url, |
542 | name: this.preferredUsername, | 528 | name: this.preferredUsername, |
543 | host: this.getHost(), | 529 | host: this.getHost(), |
544 | avatar | 530 | avatars: (this.Avatars || []).map(a => a.toFormattedJSON()), |
531 | |||
532 | // TODO: remove, deprecated in 4.2 | ||
533 | avatar: this.hasImage(ActorImageType.AVATAR) | ||
534 | ? this.Avatars[0].toFormattedJSON() | ||
535 | : undefined | ||
545 | } | 536 | } |
546 | } | 537 | } |
547 | 538 | ||
548 | toFormattedJSON (this: MActorFormattable) { | 539 | toFormattedJSON (this: MActorFormattable) { |
549 | const base = this.toFormattedSummaryJSON() | 540 | return { |
550 | 541 | ...this.toFormattedSummaryJSON(), | |
551 | let banner: ActorImage = null | ||
552 | if (this.Banner) { | ||
553 | banner = this.Banner.toFormattedJSON() | ||
554 | } | ||
555 | 542 | ||
556 | return Object.assign(base, { | ||
557 | id: this.id, | 543 | id: this.id, |
558 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 544 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
559 | followingCount: this.followingCount, | 545 | followingCount: this.followingCount, |
560 | followersCount: this.followersCount, | 546 | followersCount: this.followersCount, |
561 | banner, | 547 | createdAt: this.getCreatedAt(), |
562 | createdAt: this.getCreatedAt() | 548 | |
563 | }) | 549 | banners: (this.Banners || []).map(b => b.toFormattedJSON()), |
550 | |||
551 | // TODO: remove, deprecated in 4.2 | ||
552 | banner: this.hasImage(ActorImageType.BANNER) | ||
553 | ? this.Banners[0].toFormattedJSON() | ||
554 | : undefined | ||
555 | } | ||
564 | } | 556 | } |
565 | 557 | ||
566 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { | 558 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
567 | let icon: ActivityIconObject | 559 | let icon: ActivityIconObject |
560 | let icons: ActivityIconObject[] | ||
568 | let image: ActivityIconObject | 561 | let image: ActivityIconObject |
569 | 562 | ||
570 | if (this.avatarId) { | 563 | if (this.hasImage(ActorImageType.AVATAR)) { |
571 | const extension = getLowercaseExtension(this.Avatar.filename) | 564 | icon = getBiggestActorImage(this.Avatars).toActivityPubObject() |
572 | 565 | icons = this.Avatars.map(a => a.toActivityPubObject()) | |
573 | icon = { | ||
574 | type: 'Image', | ||
575 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
576 | height: this.Avatar.height, | ||
577 | width: this.Avatar.width, | ||
578 | url: this.getAvatarUrl() | ||
579 | } | ||
580 | } | 566 | } |
581 | 567 | ||
582 | if (this.bannerId) { | 568 | if (this.hasImage(ActorImageType.BANNER)) { |
583 | const banner = (this as MActorAPChannel).Banner | 569 | const banner = getBiggestActorImage((this as MActorAPChannel).Banners) |
584 | const extension = getLowercaseExtension(banner.filename) | 570 | const extension = getLowercaseExtension(banner.filename) |
585 | 571 | ||
586 | image = { | 572 | image = { |
@@ -588,7 +574,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
588 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | 574 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
589 | height: banner.height, | 575 | height: banner.height, |
590 | width: banner.width, | 576 | width: banner.width, |
591 | url: this.getBannerUrl() | 577 | url: ActorImageModel.getImageUrl(banner) |
592 | } | 578 | } |
593 | } | 579 | } |
594 | 580 | ||
@@ -612,7 +598,10 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
612 | publicKeyPem: this.publicKey | 598 | publicKeyPem: this.publicKey |
613 | }, | 599 | }, |
614 | published: this.getCreatedAt().toISOString(), | 600 | published: this.getCreatedAt().toISOString(), |
601 | |||
615 | icon, | 602 | icon, |
603 | icons, | ||
604 | |||
616 | image | 605 | image |
617 | } | 606 | } |
618 | 607 | ||
@@ -677,16 +666,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
677 | return this.Server ? this.Server.redundancyAllowed : false | 666 | return this.Server ? this.Server.redundancyAllowed : false |
678 | } | 667 | } |
679 | 668 | ||
680 | getAvatarUrl () { | 669 | hasImage (type: ActorImageType) { |
681 | if (!this.avatarId) return undefined | 670 | const images = type === ActorImageType.AVATAR |
682 | 671 | ? this.Avatars | |
683 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 672 | : this.Banners |
684 | } | ||
685 | |||
686 | getBannerUrl () { | ||
687 | if (!this.bannerId) return undefined | ||
688 | 673 | ||
689 | return WEBSERVER.URL + this.Banner.getStaticPath() | 674 | return Array.isArray(images) && images.length !== 0 |
690 | } | 675 | } |
691 | 676 | ||
692 | isOutdated () { | 677 | isOutdated () { |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 05083e3f7..fa5b4cc4b 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -239,11 +239,10 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> { | |||
239 | 239 | ||
240 | if (options.pluginType) query.where['type'] = options.pluginType | 240 | if (options.pluginType) query.where['type'] = options.pluginType |
241 | 241 | ||
242 | return PluginModel | 242 | return Promise.all([ |
243 | .findAndCountAll<MPlugin>(query) | 243 | PluginModel.count(query), |
244 | .then(({ rows, count }) => { | 244 | PluginModel.findAll<MPlugin>(query) |
245 | return { total: count, data: rows } | 245 | ]).then(([ total, data ]) => ({ total, data })) |
246 | }) | ||
247 | } | 246 | } |
248 | 247 | ||
249 | static listInstalled (): Promise<MPlugin[]> { | 248 | static listInstalled (): Promise<MPlugin[]> { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9f64eeb7f..9752dfbc3 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | 3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
@@ -169,16 +169,15 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo | |||
169 | order: getSort(sort), | 169 | order: getSort(sort), |
170 | where: { | 170 | where: { |
171 | accountId, | 171 | accountId, |
172 | |||
172 | ...searchAttribute(search, '$BlockedServer.host$') | 173 | ...searchAttribute(search, '$BlockedServer.host$') |
173 | } | 174 | } |
174 | } | 175 | } |
175 | 176 | ||
176 | return ServerBlocklistModel | 177 | return Promise.all([ |
177 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) | 178 | ServerBlocklistModel.scope(ScopeNames.WITH_SERVER).count(query), |
178 | .findAndCountAll<MServerBlocklistAccountServer>(query) | 179 | ServerBlocklistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]).findAll<MServerBlocklistAccountServer>(query) |
179 | .then(({ rows, count }) => { | 180 | ]).then(([ total, data ]) => ({ total, data })) |
180 | return { total: count, data: rows } | ||
181 | }) | ||
182 | } | 181 | } |
183 | 182 | ||
184 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { | 183 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 5b97510e0..802404555 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './model-builder' | ||
1 | export * from './query' | 2 | export * from './query' |
2 | export * from './update' | 3 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts new file mode 100644 index 000000000..c015ca4f5 --- /dev/null +++ b/server/models/shared/model-builder.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import { isPlainObject } from 'lodash' | ||
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | ||
6 | private readonly modelRegistry = new Map<string, T>() | ||
7 | |||
8 | constructor (private readonly sequelize: Sequelize) { | ||
9 | |||
10 | } | ||
11 | |||
12 | createModels (jsonArray: any[], baseModelName: string): T[] { | ||
13 | const result: T[] = [] | ||
14 | |||
15 | for (const json of jsonArray) { | ||
16 | const { created, model } = this.createModel(json, baseModelName, json.id + '.' + baseModelName) | ||
17 | |||
18 | if (created) result.push(model) | ||
19 | } | ||
20 | |||
21 | return result | ||
22 | } | ||
23 | |||
24 | private createModel (json: any, modelName: string, keyPath: string) { | ||
25 | if (!json.id) return { created: false, model: null } | ||
26 | |||
27 | const { created, model } = this.createOrFindModel(json, modelName, keyPath) | ||
28 | |||
29 | for (const key of Object.keys(json)) { | ||
30 | const value = json[key] | ||
31 | if (!value) continue | ||
32 | |||
33 | // Child model | ||
34 | if (isPlainObject(value)) { | ||
35 | const { created, model: subModel } = this.createModel(value, key, keyPath + '.' + json.id + '.' + key) | ||
36 | if (!created || !subModel) continue | ||
37 | |||
38 | const Model = this.findModelBuilder(modelName) | ||
39 | const association = Model.associations[key] | ||
40 | |||
41 | if (!association) { | ||
42 | logger.error('Cannot find association %s of model %s', key, modelName, { associations: Object.keys(Model.associations) }) | ||
43 | continue | ||
44 | } | ||
45 | |||
46 | if (association.isMultiAssociation) { | ||
47 | if (!Array.isArray(model[key])) model[key] = [] | ||
48 | |||
49 | model[key].push(subModel) | ||
50 | } else { | ||
51 | model[key] = subModel | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | return { created, model } | ||
57 | } | ||
58 | |||
59 | private createOrFindModel (json: any, modelName: string, keyPath: string) { | ||
60 | const registryKey = this.getModelRegistryKey(json, keyPath) | ||
61 | if (this.modelRegistry.has(registryKey)) { | ||
62 | return { | ||
63 | created: false, | ||
64 | model: this.modelRegistry.get(registryKey) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | const Model = this.findModelBuilder(modelName) | ||
69 | |||
70 | if (!Model) { | ||
71 | logger.error( | ||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | ||
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | ||
74 | ) | ||
75 | return undefined | ||
76 | } | ||
77 | |||
78 | // FIXME: typings | ||
79 | const model = new (Model as any)(json) | ||
80 | this.modelRegistry.set(registryKey, model) | ||
81 | |||
82 | return { created: true, model } | ||
83 | } | ||
84 | |||
85 | private findModelBuilder (modelName: string) { | ||
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | ||
87 | } | ||
88 | |||
89 | private buildSequelizeModelName (modelName: string) { | ||
90 | if (modelName === 'Avatars') return 'ActorImageModel' | ||
91 | if (modelName === 'ActorFollowing') return 'ActorModel' | ||
92 | if (modelName === 'ActorFollower') return 'ActorModel' | ||
93 | if (modelName === 'FlaggedAccount') return 'AccountModel' | ||
94 | |||
95 | return modelName + 'Model' | ||
96 | } | ||
97 | |||
98 | private getModelRegistryKey (json: any, keyPath: string) { | ||
99 | return keyPath + json.id | ||
100 | } | ||
101 | } | ||
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts new file mode 100644 index 000000000..9eae4fc22 --- /dev/null +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -0,0 +1,269 @@ | |||
1 | import { QueryTypes, Sequelize } from 'sequelize' | ||
2 | import { ModelBuilder } from '@server/models/shared' | ||
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | |||
7 | export interface ListNotificationsOptions { | ||
8 | userId: number | ||
9 | unread?: boolean | ||
10 | sort: string | ||
11 | offset: number | ||
12 | limit: number | ||
13 | sequelize: Sequelize | ||
14 | } | ||
15 | |||
16 | export class UserNotificationListQueryBuilder { | ||
17 | private innerQuery: string | ||
18 | private replacements: any = {} | ||
19 | private query: string | ||
20 | |||
21 | constructor (private readonly options: ListNotificationsOptions) { | ||
22 | |||
23 | } | ||
24 | |||
25 | async listNotifications () { | ||
26 | this.buildQuery() | ||
27 | |||
28 | const results = await this.options.sequelize.query(this.query, { | ||
29 | replacements: this.replacements, | ||
30 | type: QueryTypes.SELECT, | ||
31 | nest: true | ||
32 | }) | ||
33 | |||
34 | const modelBuilder = new ModelBuilder<UserNotificationModelForApi>(this.options.sequelize) | ||
35 | |||
36 | return modelBuilder.createModels(results, 'UserNotification') | ||
37 | } | ||
38 | |||
39 | private buildInnerQuery () { | ||
40 | this.innerQuery = `SELECT * FROM "userNotification" AS "UserNotificationModel" ` + | ||
41 | `${this.getWhere()} ` + | ||
42 | `${this.getOrder()} ` + | ||
43 | `LIMIT :limit OFFSET :offset ` | ||
44 | |||
45 | this.replacements.limit = this.options.limit | ||
46 | this.replacements.offset = this.options.offset | ||
47 | } | ||
48 | |||
49 | private buildQuery () { | ||
50 | this.buildInnerQuery() | ||
51 | |||
52 | this.query = ` | ||
53 | ${this.getSelect()} | ||
54 | FROM (${this.innerQuery}) "UserNotificationModel" | ||
55 | ${this.getJoins()} | ||
56 | ${this.getOrder()}` | ||
57 | } | ||
58 | |||
59 | private getWhere () { | ||
60 | let base = '"UserNotificationModel"."userId" = :userId ' | ||
61 | this.replacements.userId = this.options.userId | ||
62 | |||
63 | if (this.options.unread === true) { | ||
64 | base += 'AND "UserNotificationModel"."read" IS FALSE ' | ||
65 | } else if (this.options.unread === false) { | ||
66 | base += 'AND "UserNotificationModel"."read" IS TRUE ' | ||
67 | } | ||
68 | |||
69 | return `WHERE ${base}` | ||
70 | } | ||
71 | |||
72 | private getOrder () { | ||
73 | const orders = getSort(this.options.sort) | ||
74 | |||
75 | return 'ORDER BY ' + orders.map(o => `"UserNotificationModel"."${o[0]}" ${o[1]}`).join(', ') | ||
76 | } | ||
77 | |||
78 | private getSelect () { | ||
79 | return `SELECT | ||
80 | "UserNotificationModel"."id", | ||
81 | "UserNotificationModel"."type", | ||
82 | "UserNotificationModel"."read", | ||
83 | "UserNotificationModel"."createdAt", | ||
84 | "UserNotificationModel"."updatedAt", | ||
85 | "Video"."id" AS "Video.id", | ||
86 | "Video"."uuid" AS "Video.uuid", | ||
87 | "Video"."name" AS "Video.name", | ||
88 | "Video->VideoChannel"."id" AS "Video.VideoChannel.id", | ||
89 | "Video->VideoChannel"."name" AS "Video.VideoChannel.name", | ||
90 | "Video->VideoChannel->Actor"."id" AS "Video.VideoChannel.Actor.id", | ||
91 | "Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername", | ||
92 | "Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id", | ||
93 | "Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width", | ||
94 | "Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename", | ||
95 | "Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id", | ||
96 | "Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host", | ||
97 | "VideoComment"."id" AS "VideoComment.id", | ||
98 | "VideoComment"."originCommentId" AS "VideoComment.originCommentId", | ||
99 | "VideoComment->Account"."id" AS "VideoComment.Account.id", | ||
100 | "VideoComment->Account"."name" AS "VideoComment.Account.name", | ||
101 | "VideoComment->Account->Actor"."id" AS "VideoComment.Account.Actor.id", | ||
102 | "VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername", | ||
103 | "VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id", | ||
104 | "VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width", | ||
105 | "VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename", | ||
106 | "VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id", | ||
107 | "VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host", | ||
108 | "VideoComment->Video"."id" AS "VideoComment.Video.id", | ||
109 | "VideoComment->Video"."uuid" AS "VideoComment.Video.uuid", | ||
110 | "VideoComment->Video"."name" AS "VideoComment.Video.name", | ||
111 | "Abuse"."id" AS "Abuse.id", | ||
112 | "Abuse"."state" AS "Abuse.state", | ||
113 | "Abuse->VideoAbuse"."id" AS "Abuse.VideoAbuse.id", | ||
114 | "Abuse->VideoAbuse->Video"."id" AS "Abuse.VideoAbuse.Video.id", | ||
115 | "Abuse->VideoAbuse->Video"."uuid" AS "Abuse.VideoAbuse.Video.uuid", | ||
116 | "Abuse->VideoAbuse->Video"."name" AS "Abuse.VideoAbuse.Video.name", | ||
117 | "Abuse->VideoCommentAbuse"."id" AS "Abuse.VideoCommentAbuse.id", | ||
118 | "Abuse->VideoCommentAbuse->VideoComment"."id" AS "Abuse.VideoCommentAbuse.VideoComment.id", | ||
119 | "Abuse->VideoCommentAbuse->VideoComment"."originCommentId" AS "Abuse.VideoCommentAbuse.VideoComment.originCommentId", | ||
120 | "Abuse->VideoCommentAbuse->VideoComment->Video"."id" AS "Abuse.VideoCommentAbuse.VideoComment.Video.id", | ||
121 | "Abuse->VideoCommentAbuse->VideoComment->Video"."name" AS "Abuse.VideoCommentAbuse.VideoComment.Video.name", | ||
122 | "Abuse->VideoCommentAbuse->VideoComment->Video"."uuid" AS "Abuse.VideoCommentAbuse.VideoComment.Video.uuid", | ||
123 | "Abuse->FlaggedAccount"."id" AS "Abuse.FlaggedAccount.id", | ||
124 | "Abuse->FlaggedAccount"."name" AS "Abuse.FlaggedAccount.name", | ||
125 | "Abuse->FlaggedAccount"."description" AS "Abuse.FlaggedAccount.description", | ||
126 | "Abuse->FlaggedAccount"."actorId" AS "Abuse.FlaggedAccount.actorId", | ||
127 | "Abuse->FlaggedAccount"."userId" AS "Abuse.FlaggedAccount.userId", | ||
128 | "Abuse->FlaggedAccount"."applicationId" AS "Abuse.FlaggedAccount.applicationId", | ||
129 | "Abuse->FlaggedAccount"."createdAt" AS "Abuse.FlaggedAccount.createdAt", | ||
130 | "Abuse->FlaggedAccount"."updatedAt" AS "Abuse.FlaggedAccount.updatedAt", | ||
131 | "Abuse->FlaggedAccount->Actor"."id" AS "Abuse.FlaggedAccount.Actor.id", | ||
132 | "Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername", | ||
133 | "Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id", | ||
134 | "Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width", | ||
135 | "Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename", | ||
136 | "Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id", | ||
137 | "Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host", | ||
138 | "VideoBlacklist"."id" AS "VideoBlacklist.id", | ||
139 | "VideoBlacklist->Video"."id" AS "VideoBlacklist.Video.id", | ||
140 | "VideoBlacklist->Video"."uuid" AS "VideoBlacklist.Video.uuid", | ||
141 | "VideoBlacklist->Video"."name" AS "VideoBlacklist.Video.name", | ||
142 | "VideoImport"."id" AS "VideoImport.id", | ||
143 | "VideoImport"."magnetUri" AS "VideoImport.magnetUri", | ||
144 | "VideoImport"."targetUrl" AS "VideoImport.targetUrl", | ||
145 | "VideoImport"."torrentName" AS "VideoImport.torrentName", | ||
146 | "VideoImport->Video"."id" AS "VideoImport.Video.id", | ||
147 | "VideoImport->Video"."uuid" AS "VideoImport.Video.uuid", | ||
148 | "VideoImport->Video"."name" AS "VideoImport.Video.name", | ||
149 | "Plugin"."id" AS "Plugin.id", | ||
150 | "Plugin"."name" AS "Plugin.name", | ||
151 | "Plugin"."type" AS "Plugin.type", | ||
152 | "Plugin"."latestVersion" AS "Plugin.latestVersion", | ||
153 | "Application"."id" AS "Application.id", | ||
154 | "Application"."latestPeerTubeVersion" AS "Application.latestPeerTubeVersion", | ||
155 | "ActorFollow"."id" AS "ActorFollow.id", | ||
156 | "ActorFollow"."state" AS "ActorFollow.state", | ||
157 | "ActorFollow->ActorFollower"."id" AS "ActorFollow.ActorFollower.id", | ||
158 | "ActorFollow->ActorFollower"."preferredUsername" AS "ActorFollow.ActorFollower.preferredUsername", | ||
159 | "ActorFollow->ActorFollower->Account"."id" AS "ActorFollow.ActorFollower.Account.id", | ||
160 | "ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name", | ||
161 | "ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id", | ||
162 | "ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width", | ||
163 | "ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename", | ||
164 | "ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id", | ||
165 | "ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host", | ||
166 | "ActorFollow->ActorFollowing"."id" AS "ActorFollow.ActorFollowing.id", | ||
167 | "ActorFollow->ActorFollowing"."preferredUsername" AS "ActorFollow.ActorFollowing.preferredUsername", | ||
168 | "ActorFollow->ActorFollowing"."type" AS "ActorFollow.ActorFollowing.type", | ||
169 | "ActorFollow->ActorFollowing->VideoChannel"."id" AS "ActorFollow.ActorFollowing.VideoChannel.id", | ||
170 | "ActorFollow->ActorFollowing->VideoChannel"."name" AS "ActorFollow.ActorFollowing.VideoChannel.name", | ||
171 | "ActorFollow->ActorFollowing->Account"."id" AS "ActorFollow.ActorFollowing.Account.id", | ||
172 | "ActorFollow->ActorFollowing->Account"."name" AS "ActorFollow.ActorFollowing.Account.name", | ||
173 | "ActorFollow->ActorFollowing->Server"."id" AS "ActorFollow.ActorFollowing.Server.id", | ||
174 | "ActorFollow->ActorFollowing->Server"."host" AS "ActorFollow.ActorFollowing.Server.host", | ||
175 | "Account"."id" AS "Account.id", | ||
176 | "Account"."name" AS "Account.name", | ||
177 | "Account->Actor"."id" AS "Account.Actor.id", | ||
178 | "Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername", | ||
179 | "Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id", | ||
180 | "Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width", | ||
181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | ||
182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | ||
183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | ||
184 | } | ||
185 | |||
186 | private getJoins () { | ||
187 | return ` | ||
188 | LEFT JOIN ( | ||
189 | "video" AS "Video" | ||
190 | INNER JOIN "videoChannel" AS "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" | ||
191 | INNER JOIN "actor" AS "Video->VideoChannel->Actor" ON "Video->VideoChannel"."actorId" = "Video->VideoChannel->Actor"."id" | ||
192 | LEFT JOIN "actorImage" AS "Video->VideoChannel->Actor->Avatars" | ||
193 | ON "Video->VideoChannel->Actor"."id" = "Video->VideoChannel->Actor->Avatars"."actorId" | ||
194 | AND "Video->VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
195 | LEFT JOIN "server" AS "Video->VideoChannel->Actor->Server" | ||
196 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" | ||
197 | ) ON "UserNotificationModel"."videoId" = "Video"."id" | ||
198 | |||
199 | LEFT JOIN ( | ||
200 | "videoComment" AS "VideoComment" | ||
201 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" | ||
202 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" | ||
203 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" | ||
204 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" | ||
205 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
206 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" | ||
207 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" | ||
208 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" | ||
209 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" | ||
210 | |||
211 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | ||
212 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | ||
213 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | ||
214 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | ||
215 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
216 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
217 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
218 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
219 | LEFT JOIN ( | ||
220 | "account" AS "Abuse->FlaggedAccount" | ||
221 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
222 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
223 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
224 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
225 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
226 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
227 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
228 | |||
229 | LEFT JOIN ( | ||
230 | "videoBlacklist" AS "VideoBlacklist" | ||
231 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" | ||
232 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" | ||
233 | |||
234 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" | ||
235 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" | ||
236 | |||
237 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" | ||
238 | |||
239 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" | ||
240 | |||
241 | LEFT JOIN ( | ||
242 | "actorFollow" AS "ActorFollow" | ||
243 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | ||
244 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | ||
245 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | ||
246 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | ||
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | ||
248 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | ||
249 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
250 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
251 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
252 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
253 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
254 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
256 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
257 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
258 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
259 | |||
260 | LEFT JOIN ( | ||
261 | "account" AS "Account" | ||
262 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" | ||
263 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" | ||
264 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" | ||
265 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
266 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" | ||
267 | ) ON "UserNotificationModel"."accountId" = "Account"."id"` | ||
268 | } | ||
269 | } | ||
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index edad10a55..eca127e7e 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 4 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { uuidToShort } from '@shared/extra-utils' | 5 | import { uuidToShort } from '@shared/extra-utils' |
5 | import { UserNotification, UserNotificationType } from '@shared/models' | 6 | import { UserNotification, UserNotificationType } from '@shared/models' |
@@ -7,207 +8,18 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
7 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 8 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
8 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 9 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
9 | import { AbuseModel } from '../abuse/abuse' | 10 | import { AbuseModel } from '../abuse/abuse' |
10 | import { VideoAbuseModel } from '../abuse/video-abuse' | ||
11 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
12 | import { AccountModel } from '../account/account' | 11 | import { AccountModel } from '../account/account' |
13 | import { ActorModel } from '../actor/actor' | ||
14 | import { ActorFollowModel } from '../actor/actor-follow' | 12 | import { ActorFollowModel } from '../actor/actor-follow' |
15 | import { ActorImageModel } from '../actor/actor-image' | ||
16 | import { ApplicationModel } from '../application/application' | 13 | import { ApplicationModel } from '../application/application' |
17 | import { PluginModel } from '../server/plugin' | 14 | import { PluginModel } from '../server/plugin' |
18 | import { ServerModel } from '../server/server' | 15 | import { throwIfNotValid } from '../utils' |
19 | import { getSort, throwIfNotValid } from '../utils' | ||
20 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
21 | import { VideoBlacklistModel } from '../video/video-blacklist' | 17 | import { VideoBlacklistModel } from '../video/video-blacklist' |
22 | import { VideoChannelModel } from '../video/video-channel' | ||
23 | import { VideoCommentModel } from '../video/video-comment' | 18 | import { VideoCommentModel } from '../video/video-comment' |
24 | import { VideoImportModel } from '../video/video-import' | 19 | import { VideoImportModel } from '../video/video-import' |
20 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' | ||
25 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
26 | 22 | ||
27 | enum ScopeNames { | ||
28 | WITH_ALL = 'WITH_ALL' | ||
29 | } | ||
30 | |||
31 | function buildActorWithAvatarInclude () { | ||
32 | return { | ||
33 | attributes: [ 'preferredUsername' ], | ||
34 | model: ActorModel.unscoped(), | ||
35 | required: true, | ||
36 | include: [ | ||
37 | { | ||
38 | attributes: [ 'filename' ], | ||
39 | as: 'Avatar', | ||
40 | model: ActorImageModel.unscoped(), | ||
41 | required: false | ||
42 | }, | ||
43 | { | ||
44 | attributes: [ 'host' ], | ||
45 | model: ServerModel.unscoped(), | ||
46 | required: false | ||
47 | } | ||
48 | ] | ||
49 | } | ||
50 | } | ||
51 | |||
52 | function buildVideoInclude (required: boolean) { | ||
53 | return { | ||
54 | attributes: [ 'id', 'uuid', 'name' ], | ||
55 | model: VideoModel.unscoped(), | ||
56 | required | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function buildChannelInclude (required: boolean, withActor = false) { | ||
61 | return { | ||
62 | required, | ||
63 | attributes: [ 'id', 'name' ], | ||
64 | model: VideoChannelModel.unscoped(), | ||
65 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
66 | } | ||
67 | } | ||
68 | |||
69 | function buildAccountInclude (required: boolean, withActor = false) { | ||
70 | return { | ||
71 | required, | ||
72 | attributes: [ 'id', 'name' ], | ||
73 | model: AccountModel.unscoped(), | ||
74 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
75 | } | ||
76 | } | ||
77 | |||
78 | @Scopes(() => ({ | ||
79 | [ScopeNames.WITH_ALL]: { | ||
80 | include: [ | ||
81 | Object.assign(buildVideoInclude(false), { | ||
82 | include: [ buildChannelInclude(true, true) ] | ||
83 | }), | ||
84 | |||
85 | { | ||
86 | attributes: [ 'id', 'originCommentId' ], | ||
87 | model: VideoCommentModel.unscoped(), | ||
88 | required: false, | ||
89 | include: [ | ||
90 | buildAccountInclude(true, true), | ||
91 | buildVideoInclude(true) | ||
92 | ] | ||
93 | }, | ||
94 | |||
95 | { | ||
96 | attributes: [ 'id', 'state' ], | ||
97 | model: AbuseModel.unscoped(), | ||
98 | required: false, | ||
99 | include: [ | ||
100 | { | ||
101 | attributes: [ 'id' ], | ||
102 | model: VideoAbuseModel.unscoped(), | ||
103 | required: false, | ||
104 | include: [ buildVideoInclude(false) ] | ||
105 | }, | ||
106 | { | ||
107 | attributes: [ 'id' ], | ||
108 | model: VideoCommentAbuseModel.unscoped(), | ||
109 | required: false, | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: [ 'id', 'originCommentId' ], | ||
113 | model: VideoCommentModel.unscoped(), | ||
114 | required: false, | ||
115 | include: [ | ||
116 | { | ||
117 | attributes: [ 'id', 'name', 'uuid' ], | ||
118 | model: VideoModel.unscoped(), | ||
119 | required: false | ||
120 | } | ||
121 | ] | ||
122 | } | ||
123 | ] | ||
124 | }, | ||
125 | { | ||
126 | model: AccountModel, | ||
127 | as: 'FlaggedAccount', | ||
128 | required: false, | ||
129 | include: [ buildActorWithAvatarInclude() ] | ||
130 | } | ||
131 | ] | ||
132 | }, | ||
133 | |||
134 | { | ||
135 | attributes: [ 'id' ], | ||
136 | model: VideoBlacklistModel.unscoped(), | ||
137 | required: false, | ||
138 | include: [ buildVideoInclude(true) ] | ||
139 | }, | ||
140 | |||
141 | { | ||
142 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | ||
143 | model: VideoImportModel.unscoped(), | ||
144 | required: false, | ||
145 | include: [ buildVideoInclude(false) ] | ||
146 | }, | ||
147 | |||
148 | { | ||
149 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
150 | model: PluginModel.unscoped(), | ||
151 | required: false | ||
152 | }, | ||
153 | |||
154 | { | ||
155 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
156 | model: ApplicationModel.unscoped(), | ||
157 | required: false | ||
158 | }, | ||
159 | |||
160 | { | ||
161 | attributes: [ 'id', 'state' ], | ||
162 | model: ActorFollowModel.unscoped(), | ||
163 | required: false, | ||
164 | include: [ | ||
165 | { | ||
166 | attributes: [ 'preferredUsername' ], | ||
167 | model: ActorModel.unscoped(), | ||
168 | required: true, | ||
169 | as: 'ActorFollower', | ||
170 | include: [ | ||
171 | { | ||
172 | attributes: [ 'id', 'name' ], | ||
173 | model: AccountModel.unscoped(), | ||
174 | required: true | ||
175 | }, | ||
176 | { | ||
177 | attributes: [ 'filename' ], | ||
178 | as: 'Avatar', | ||
179 | model: ActorImageModel.unscoped(), | ||
180 | required: false | ||
181 | }, | ||
182 | { | ||
183 | attributes: [ 'host' ], | ||
184 | model: ServerModel.unscoped(), | ||
185 | required: false | ||
186 | } | ||
187 | ] | ||
188 | }, | ||
189 | { | ||
190 | attributes: [ 'preferredUsername', 'type' ], | ||
191 | model: ActorModel.unscoped(), | ||
192 | required: true, | ||
193 | as: 'ActorFollowing', | ||
194 | include: [ | ||
195 | buildChannelInclude(false), | ||
196 | buildAccountInclude(false), | ||
197 | { | ||
198 | attributes: [ 'host' ], | ||
199 | model: ServerModel.unscoped(), | ||
200 | required: false | ||
201 | } | ||
202 | ] | ||
203 | } | ||
204 | ] | ||
205 | }, | ||
206 | |||
207 | buildAccountInclude(false, true) | ||
208 | ] | ||
209 | } | ||
210 | })) | ||
211 | @Table({ | 23 | @Table({ |
212 | tableName: 'userNotification', | 24 | tableName: 'userNotification', |
213 | indexes: [ | 25 | indexes: [ |
@@ -342,7 +154,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
342 | }, | 154 | }, |
343 | onDelete: 'cascade' | 155 | onDelete: 'cascade' |
344 | }) | 156 | }) |
345 | Comment: VideoCommentModel | 157 | VideoComment: VideoCommentModel |
346 | 158 | ||
347 | @ForeignKey(() => AbuseModel) | 159 | @ForeignKey(() => AbuseModel) |
348 | @Column | 160 | @Column |
@@ -431,11 +243,14 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
431 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 243 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
432 | const where = { userId } | 244 | const where = { userId } |
433 | 245 | ||
434 | const query: FindOptions = { | 246 | const query = { |
247 | userId, | ||
248 | unread, | ||
435 | offset: start, | 249 | offset: start, |
436 | limit: count, | 250 | limit: count, |
437 | order: getSort(sort), | 251 | sort, |
438 | where | 252 | where, |
253 | sequelize: this.sequelize | ||
439 | } | 254 | } |
440 | 255 | ||
441 | if (unread !== undefined) query.where['read'] = !unread | 256 | if (unread !== undefined) query.where['read'] = !unread |
@@ -445,8 +260,8 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
445 | .then(count => count || 0), | 260 | .then(count => count || 0), |
446 | 261 | ||
447 | count === 0 | 262 | count === 0 |
448 | ? [] | 263 | ? [] as UserNotificationModelForApi[] |
449 | : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) | 264 | : new UserNotificationListQueryBuilder(query).listNotifications() |
450 | ]).then(([ total, data ]) => ({ total, data })) | 265 | ]).then(([ total, data ]) => ({ total, data })) |
451 | } | 266 | } |
452 | 267 | ||
@@ -524,25 +339,31 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
524 | 339 | ||
525 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { | 340 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { |
526 | const video = this.Video | 341 | const video = this.Video |
527 | ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) | 342 | ? { |
343 | ...this.formatVideo(this.Video), | ||
344 | |||
345 | channel: this.formatActor(this.Video.VideoChannel) | ||
346 | } | ||
528 | : undefined | 347 | : undefined |
529 | 348 | ||
530 | const videoImport = this.VideoImport | 349 | const videoImport = this.VideoImport |
531 | ? { | 350 | ? { |
532 | id: this.VideoImport.id, | 351 | id: this.VideoImport.id, |
533 | video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, | 352 | video: this.VideoImport.Video |
353 | ? this.formatVideo(this.VideoImport.Video) | ||
354 | : undefined, | ||
534 | torrentName: this.VideoImport.torrentName, | 355 | torrentName: this.VideoImport.torrentName, |
535 | magnetUri: this.VideoImport.magnetUri, | 356 | magnetUri: this.VideoImport.magnetUri, |
536 | targetUrl: this.VideoImport.targetUrl | 357 | targetUrl: this.VideoImport.targetUrl |
537 | } | 358 | } |
538 | : undefined | 359 | : undefined |
539 | 360 | ||
540 | const comment = this.Comment | 361 | const comment = this.VideoComment |
541 | ? { | 362 | ? { |
542 | id: this.Comment.id, | 363 | id: this.VideoComment.id, |
543 | threadId: this.Comment.getThreadId(), | 364 | threadId: this.VideoComment.getThreadId(), |
544 | account: this.formatActor(this.Comment.Account), | 365 | account: this.formatActor(this.VideoComment.Account), |
545 | video: this.formatVideo(this.Comment.Video) | 366 | video: this.formatVideo(this.VideoComment.Video) |
546 | } | 367 | } |
547 | : undefined | 368 | : undefined |
548 | 369 | ||
@@ -570,8 +391,9 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
570 | id: this.ActorFollow.ActorFollower.Account.id, | 391 | id: this.ActorFollow.ActorFollower.Account.id, |
571 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | 392 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), |
572 | name: this.ActorFollow.ActorFollower.preferredUsername, | 393 | name: this.ActorFollow.ActorFollower.preferredUsername, |
573 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, | 394 | host: this.ActorFollow.ActorFollower.getHost(), |
574 | host: this.ActorFollow.ActorFollower.getHost() | 395 | |
396 | ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars) | ||
575 | }, | 397 | }, |
576 | following: { | 398 | following: { |
577 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], | 399 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], |
@@ -612,7 +434,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
612 | } | 434 | } |
613 | } | 435 | } |
614 | 436 | ||
615 | formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { | 437 | formatVideo (video: UserNotificationIncludes.VideoInclude) { |
616 | return { | 438 | return { |
617 | id: video.id, | 439 | id: video.id, |
618 | uuid: video.uuid, | 440 | uuid: video.uuid, |
@@ -621,7 +443,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
621 | } | 443 | } |
622 | } | 444 | } |
623 | 445 | ||
624 | formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { | 446 | formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) { |
625 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment | 447 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment |
626 | ? { | 448 | ? { |
627 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 449 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
@@ -637,9 +459,13 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
637 | } | 459 | } |
638 | : undefined | 460 | : undefined |
639 | 461 | ||
640 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 462 | const videoAbuse = abuse.VideoAbuse?.Video |
463 | ? this.formatVideo(abuse.VideoAbuse.Video) | ||
464 | : undefined | ||
641 | 465 | ||
642 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined | 466 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) |
467 | ? this.formatActor(abuse.FlaggedAccount) | ||
468 | : undefined | ||
643 | 469 | ||
644 | return { | 470 | return { |
645 | id: abuse.id, | 471 | id: abuse.id, |
@@ -651,19 +477,32 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
651 | } | 477 | } |
652 | 478 | ||
653 | formatActor ( | 479 | formatActor ( |
654 | this: UserNotificationModelForApi, | ||
655 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor | 480 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor |
656 | ) { | 481 | ) { |
657 | const avatar = accountOrChannel.Actor.Avatar | ||
658 | ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } | ||
659 | : undefined | ||
660 | |||
661 | return { | 482 | return { |
662 | id: accountOrChannel.id, | 483 | id: accountOrChannel.id, |
663 | displayName: accountOrChannel.getDisplayName(), | 484 | displayName: accountOrChannel.getDisplayName(), |
664 | name: accountOrChannel.Actor.preferredUsername, | 485 | name: accountOrChannel.Actor.preferredUsername, |
665 | host: accountOrChannel.Actor.getHost(), | 486 | host: accountOrChannel.Actor.getHost(), |
666 | avatar | 487 | |
488 | ...this.formatAvatars(accountOrChannel.Actor.Avatars) | ||
489 | } | ||
490 | } | ||
491 | |||
492 | formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) { | ||
493 | if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] } | ||
494 | |||
495 | return { | ||
496 | avatar: this.formatAvatar(getBiggestActorImage(avatars)), | ||
497 | |||
498 | avatars: avatars.map(a => this.formatAvatar(a)) | ||
499 | } | ||
500 | } | ||
501 | |||
502 | formatAvatar (a: UserNotificationIncludes.ActorImageInclude) { | ||
503 | return { | ||
504 | path: a.getStaticPath(), | ||
505 | width: a.width | ||
667 | } | 506 | } |
668 | } | 507 | } |
669 | } | 508 | } |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index ad8ce08cb..bcf56dfa1 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -106,7 +106,7 @@ enum ScopeNames { | |||
106 | include: [ | 106 | include: [ |
107 | { | 107 | { |
108 | model: ActorImageModel, | 108 | model: ActorImageModel, |
109 | as: 'Banner', | 109 | as: 'Banners', |
110 | required: false | 110 | required: false |
111 | } | 111 | } |
112 | ] | 112 | ] |
@@ -495,13 +495,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
495 | where | 495 | where |
496 | } | 496 | } |
497 | 497 | ||
498 | return UserModel.findAndCountAll(query) | 498 | return Promise.all([ |
499 | .then(({ rows, count }) => { | 499 | UserModel.unscoped().count(query), |
500 | return { | 500 | UserModel.findAll(query) |
501 | data: rows, | 501 | ]).then(([ total, data ]) => ({ total, data })) |
502 | total: count | ||
503 | } | ||
504 | }) | ||
505 | } | 502 | } |
506 | 503 | ||
507 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { | 504 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { |
diff --git a/server/models/utils.ts b/server/models/utils.ts index 66b653e3d..70bfbdb8b 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -181,7 +181,7 @@ function buildServerIdsFollowedBy (actorId: any) { | |||
181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | 181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + |
182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | 182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + |
183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
184 | ')' | 184 | ')' |
185 | } | 185 | } |
186 | 186 | ||
187 | function buildWhereIdOrUUID (id: number | string) { | 187 | function buildWhereIdOrUUID (id: number | string) { |
diff --git a/server/models/video/sql/video/index.ts b/server/models/video/sql/video/index.ts new file mode 100644 index 000000000..e9132d5e1 --- /dev/null +++ b/server/models/video/sql/video/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './video-model-get-query-builder' | ||
2 | export * from './videos-id-list-query-builder' | ||
3 | export * from './videos-model-list-query-builder' | ||
diff --git a/server/models/video/sql/shared/abstract-run-query.ts b/server/models/video/sql/video/shared/abstract-run-query.ts index 8e7a7642d..8e7a7642d 100644 --- a/server/models/video/sql/shared/abstract-run-query.ts +++ b/server/models/video/sql/video/shared/abstract-run-query.ts | |||
diff --git a/server/models/video/sql/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index a6afb04e4..490e5e6e0 100644 --- a/server/models/video/sql/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { createSafeIn } from '@server/models/utils' | 1 | import { createSafeIn } from '@server/models/utils' |
2 | import { MUserAccountId } from '@server/types/models' | 2 | import { MUserAccountId } from '@server/types/models' |
3 | import { ActorImageType } from '@shared/models' | ||
3 | import validator from 'validator' | 4 | import validator from 'validator' |
4 | import { AbstractRunQuery } from './abstract-run-query' | 5 | import { AbstractRunQuery } from './abstract-run-query' |
5 | import { VideoTableAttributes } from './video-table-attributes' | 6 | import { VideoTableAttributes } from './video-table-attributes' |
@@ -42,8 +43,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
42 | ) | 43 | ) |
43 | 44 | ||
44 | this.addJoin( | 45 | this.addJoin( |
45 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | 46 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' + |
46 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' | 47 | 'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' + |
48 | `AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
47 | ) | 49 | ) |
48 | 50 | ||
49 | this.attributes = { | 51 | this.attributes = { |
@@ -51,7 +53,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
51 | 53 | ||
52 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | 54 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), |
53 | ...this.buildActorInclude('VideoChannel->Actor'), | 55 | ...this.buildActorInclude('VideoChannel->Actor'), |
54 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), | 56 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatars'), |
55 | ...this.buildServerInclude('VideoChannel->Actor->Server') | 57 | ...this.buildServerInclude('VideoChannel->Actor->Server') |
56 | } | 58 | } |
57 | } | 59 | } |
@@ -68,8 +70,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
68 | ) | 70 | ) |
69 | 71 | ||
70 | this.addJoin( | 72 | this.addJoin( |
71 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | 73 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' + |
72 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' | 74 | 'ON "VideoChannel->Account"."actorId"= "VideoChannel->Account->Actor->Avatars"."actorId" ' + |
75 | `AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
73 | ) | 76 | ) |
74 | 77 | ||
75 | this.attributes = { | 78 | this.attributes = { |
@@ -77,7 +80,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
77 | 80 | ||
78 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), | 81 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), |
79 | ...this.buildActorInclude('VideoChannel->Account->Actor'), | 82 | ...this.buildActorInclude('VideoChannel->Account->Actor'), |
80 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), | 83 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'), |
81 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') | 84 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') |
82 | } | 85 | } |
83 | } | 86 | } |
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index 3eb3dc07d..3eb3dc07d 100644 --- a/server/models/video/sql/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 7751d8e68..b1b47b721 100644 --- a/server/models/video/sql/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -9,15 +9,15 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | |||
9 | import { TrackerModel } from '@server/models/server/tracker' | 9 | import { TrackerModel } from '@server/models/server/tracker' |
10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
11 | import { VideoInclude } from '@shared/models' | 11 | import { VideoInclude } from '@shared/models' |
12 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | 12 | import { ScheduleVideoUpdateModel } from '../../../schedule-video-update' |
13 | import { TagModel } from '../../tag' | 13 | import { TagModel } from '../../../tag' |
14 | import { ThumbnailModel } from '../../thumbnail' | 14 | import { ThumbnailModel } from '../../../thumbnail' |
15 | import { VideoModel } from '../../video' | 15 | import { VideoModel } from '../../../video' |
16 | import { VideoBlacklistModel } from '../../video-blacklist' | 16 | import { VideoBlacklistModel } from '../../../video-blacklist' |
17 | import { VideoChannelModel } from '../../video-channel' | 17 | import { VideoChannelModel } from '../../../video-channel' |
18 | import { VideoFileModel } from '../../video-file' | 18 | import { VideoFileModel } from '../../../video-file' |
19 | import { VideoLiveModel } from '../../video-live' | 19 | import { VideoLiveModel } from '../../../video-live' |
20 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | 20 | import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist' |
21 | import { VideoTableAttributes } from './video-table-attributes' | 21 | import { VideoTableAttributes } from './video-table-attributes' |
22 | 22 | ||
23 | type SQLRow = { [id: string]: string | number } | 23 | type SQLRow = { [id: string]: string | number } |
@@ -34,6 +34,7 @@ export class VideoModelBuilder { | |||
34 | private videoFileMemo: { [ id: number ]: VideoFileModel } | 34 | private videoFileMemo: { [ id: number ]: VideoFileModel } |
35 | 35 | ||
36 | private thumbnailsDone: Set<any> | 36 | private thumbnailsDone: Set<any> |
37 | private actorImagesDone: Set<any> | ||
37 | private historyDone: Set<any> | 38 | private historyDone: Set<any> |
38 | private blacklistDone: Set<any> | 39 | private blacklistDone: Set<any> |
39 | private accountBlocklistDone: Set<any> | 40 | private accountBlocklistDone: Set<any> |
@@ -69,11 +70,21 @@ export class VideoModelBuilder { | |||
69 | for (const row of rows) { | 70 | for (const row of rows) { |
70 | this.buildVideoAndAccount(row) | 71 | this.buildVideoAndAccount(row) |
71 | 72 | ||
72 | const videoModel = this.videosMemo[row.id] | 73 | const videoModel = this.videosMemo[row.id as number] |
73 | 74 | ||
74 | this.setUserHistory(row, videoModel) | 75 | this.setUserHistory(row, videoModel) |
75 | this.addThumbnail(row, videoModel) | 76 | this.addThumbnail(row, videoModel) |
76 | 77 | ||
78 | const channelActor = videoModel.VideoChannel?.Actor | ||
79 | if (channelActor) { | ||
80 | this.addActorAvatar(row, 'VideoChannel.Actor', channelActor) | ||
81 | } | ||
82 | |||
83 | const accountActor = videoModel.VideoChannel?.Account?.Actor | ||
84 | if (accountActor) { | ||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | ||
86 | } | ||
87 | |||
77 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebTorrentFiles) { |
78 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebTorrentFile(row, videoModel) |
79 | } | 90 | } |
@@ -113,6 +124,7 @@ export class VideoModelBuilder { | |||
113 | this.videoFileMemo = {} | 124 | this.videoFileMemo = {} |
114 | 125 | ||
115 | this.thumbnailsDone = new Set() | 126 | this.thumbnailsDone = new Set() |
127 | this.actorImagesDone = new Set() | ||
116 | this.historyDone = new Set() | 128 | this.historyDone = new Set() |
117 | this.blacklistDone = new Set() | 129 | this.blacklistDone = new Set() |
118 | this.liveDone = new Set() | 130 | this.liveDone = new Set() |
@@ -195,13 +207,8 @@ export class VideoModelBuilder { | |||
195 | 207 | ||
196 | private buildActor (row: SQLRow, prefix: string) { | 208 | private buildActor (row: SQLRow, prefix: string) { |
197 | const actorPrefix = `${prefix}.Actor` | 209 | const actorPrefix = `${prefix}.Actor` |
198 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
199 | const serverPrefix = `${actorPrefix}.Server` | 210 | const serverPrefix = `${actorPrefix}.Server` |
200 | 211 | ||
201 | const avatarModel = row[`${avatarPrefix}.id`] !== null | ||
202 | ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts) | ||
203 | : null | ||
204 | |||
205 | const serverModel = row[`${serverPrefix}.id`] !== null | 212 | const serverModel = row[`${serverPrefix}.id`] !== null |
206 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) | 213 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) |
207 | : null | 214 | : null |
@@ -209,8 +216,8 @@ export class VideoModelBuilder { | |||
209 | if (serverModel) serverModel.BlockedBy = [] | 216 | if (serverModel) serverModel.BlockedBy = [] |
210 | 217 | ||
211 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) | 218 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) |
212 | actorModel.Avatar = avatarModel | ||
213 | actorModel.Server = serverModel | 219 | actorModel.Server = serverModel |
220 | actorModel.Avatars = [] | ||
214 | 221 | ||
215 | return actorModel | 222 | return actorModel |
216 | } | 223 | } |
@@ -226,6 +233,18 @@ export class VideoModelBuilder { | |||
226 | this.historyDone.add(id) | 233 | this.historyDone.add(id) |
227 | } | 234 | } |
228 | 235 | ||
236 | private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) { | ||
237 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
238 | const id = row[`${avatarPrefix}.id`] | ||
239 | if (!id || this.actorImagesDone.has(id)) return | ||
240 | |||
241 | const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix) | ||
242 | const avatarModel = new ActorImageModel(attributes, this.buildOpts) | ||
243 | actor.Avatars.push(avatarModel) | ||
244 | |||
245 | this.actorImagesDone.add(id) | ||
246 | } | ||
247 | |||
229 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { | 248 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { |
230 | const id = row['Thumbnails.id'] | 249 | const id = row['Thumbnails.id'] |
231 | if (!id || this.thumbnailsDone.has(id)) return | 250 | if (!id || this.thumbnailsDone.has(id)) return |
diff --git a/server/models/video/sql/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 8a8d2073a..df2ed3fb0 100644 --- a/server/models/video/sql/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -186,8 +186,7 @@ export class VideoTableAttributes { | |||
186 | 'id', | 186 | 'id', |
187 | 'preferredUsername', | 187 | 'preferredUsername', |
188 | 'url', | 188 | 'url', |
189 | 'serverId', | 189 | 'serverId' |
190 | 'avatarId' | ||
191 | ] | 190 | ] |
192 | 191 | ||
193 | if (this.mode === 'get') { | 192 | if (this.mode === 'get') { |
@@ -212,6 +211,7 @@ export class VideoTableAttributes { | |||
212 | getAvatarAttributes () { | 211 | getAvatarAttributes () { |
213 | let attributeKeys = [ | 212 | let attributeKeys = [ |
214 | 'id', | 213 | 'id', |
214 | 'width', | ||
215 | 'filename', | 215 | 'filename', |
216 | 'type', | 216 | 'type', |
217 | 'fileUrl', | 217 | 'fileUrl', |
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index a65c96097..a65c96097 100644 --- a/server/models/video/sql/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 098e15359..098e15359 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index b15b29ec3..b15b29ec3 100644 --- a/server/models/video/sql/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 2c6669bcb..410fd6d3f 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { sendDeleteActor } from '../../lib/activitypub/send' | 32 | import { sendDeleteActor } from '../../lib/activitypub/send' |
33 | import { | 33 | import { |
34 | MChannel, | ||
34 | MChannelActor, | 35 | MChannelActor, |
35 | MChannelAP, | 36 | MChannelAP, |
36 | MChannelBannerAccountDefault, | 37 | MChannelBannerAccountDefault, |
@@ -62,6 +63,7 @@ type AvailableForListOptions = { | |||
62 | search?: string | 63 | search?: string |
63 | host?: string | 64 | host?: string |
64 | handles?: string[] | 65 | handles?: string[] |
66 | forCount?: boolean | ||
65 | } | 67 | } |
66 | 68 | ||
67 | type AvailableWithStatsOptions = { | 69 | type AvailableWithStatsOptions = { |
@@ -116,70 +118,91 @@ export type SummaryOptions = { | |||
116 | }) | 118 | }) |
117 | } | 119 | } |
118 | 120 | ||
119 | let rootWhere: WhereOptions | 121 | if (Array.isArray(options.handles) && options.handles.length !== 0) { |
120 | if (options.handles) { | 122 | const or: string[] = [] |
121 | const or: WhereOptions[] = [] | ||
122 | 123 | ||
123 | for (const handle of options.handles || []) { | 124 | for (const handle of options.handles || []) { |
124 | const [ preferredUsername, host ] = handle.split('@') | 125 | const [ preferredUsername, host ] = handle.split('@') |
125 | 126 | ||
126 | if (!host || host === WEBSERVER.HOST) { | 127 | if (!host || host === WEBSERVER.HOST) { |
127 | or.push({ | 128 | or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`) |
128 | '$Actor.preferredUsername$': preferredUsername, | ||
129 | '$Actor.serverId$': null | ||
130 | }) | ||
131 | } else { | 129 | } else { |
132 | or.push({ | 130 | or.push( |
133 | '$Actor.preferredUsername$': preferredUsername, | 131 | `(` + |
134 | '$Actor.Server.host$': host | 132 | `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` + |
135 | }) | 133 | `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` + |
134 | `)` | ||
135 | ) | ||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | rootWhere = { | 139 | whereActorAnd.push({ |
140 | [Op.or]: or | 140 | id: { |
141 | } | 141 | [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`) |
142 | } | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | const channelInclude: Includeable[] = [] | ||
147 | const accountInclude: Includeable[] = [] | ||
148 | |||
149 | if (options.forCount !== true) { | ||
150 | accountInclude.push({ | ||
151 | model: ServerModel, | ||
152 | required: false | ||
153 | }) | ||
154 | |||
155 | accountInclude.push({ | ||
156 | model: ActorImageModel, | ||
157 | as: 'Avatars', | ||
158 | required: false | ||
159 | }) | ||
160 | |||
161 | channelInclude.push({ | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatars', | ||
164 | required: false | ||
165 | }) | ||
166 | |||
167 | channelInclude.push({ | ||
168 | model: ActorImageModel, | ||
169 | as: 'Banners', | ||
170 | required: false | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | if (options.forCount !== true || serverRequired) { | ||
175 | channelInclude.push({ | ||
176 | model: ServerModel, | ||
177 | duplicating: false, | ||
178 | required: serverRequired, | ||
179 | where: whereServer | ||
180 | }) | ||
142 | } | 181 | } |
143 | 182 | ||
144 | return { | 183 | return { |
145 | where: rootWhere, | ||
146 | include: [ | 184 | include: [ |
147 | { | 185 | { |
148 | attributes: { | 186 | attributes: { |
149 | exclude: unusedActorAttributesForAPI | 187 | exclude: unusedActorAttributesForAPI |
150 | }, | 188 | }, |
151 | model: ActorModel, | 189 | model: ActorModel.unscoped(), |
152 | where: { | 190 | where: { |
153 | [Op.and]: whereActorAnd | 191 | [Op.and]: whereActorAnd |
154 | }, | 192 | }, |
155 | include: [ | 193 | include: channelInclude |
156 | { | ||
157 | model: ServerModel, | ||
158 | required: serverRequired, | ||
159 | where: whereServer | ||
160 | }, | ||
161 | { | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatar', | ||
164 | required: false | ||
165 | }, | ||
166 | { | ||
167 | model: ActorImageModel, | ||
168 | as: 'Banner', | ||
169 | required: false | ||
170 | } | ||
171 | ] | ||
172 | }, | 194 | }, |
173 | { | 195 | { |
174 | model: AccountModel, | 196 | model: AccountModel.unscoped(), |
175 | required: true, | 197 | required: true, |
176 | include: [ | 198 | include: [ |
177 | { | 199 | { |
178 | attributes: { | 200 | attributes: { |
179 | exclude: unusedActorAttributesForAPI | 201 | exclude: unusedActorAttributesForAPI |
180 | }, | 202 | }, |
181 | model: ActorModel, // Default scope includes avatar and server | 203 | model: ActorModel.unscoped(), |
182 | required: true | 204 | required: true, |
205 | include: accountInclude | ||
183 | } | 206 | } |
184 | ] | 207 | ] |
185 | } | 208 | } |
@@ -189,7 +212,7 @@ export type SummaryOptions = { | |||
189 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | 212 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
190 | const include: Includeable[] = [ | 213 | const include: Includeable[] = [ |
191 | { | 214 | { |
192 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 215 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
193 | model: ActorModel.unscoped(), | 216 | model: ActorModel.unscoped(), |
194 | required: options.actorRequired ?? true, | 217 | required: options.actorRequired ?? true, |
195 | include: [ | 218 | include: [ |
@@ -199,8 +222,8 @@ export type SummaryOptions = { | |||
199 | required: false | 222 | required: false |
200 | }, | 223 | }, |
201 | { | 224 | { |
202 | model: ActorImageModel.unscoped(), | 225 | model: ActorImageModel, |
203 | as: 'Avatar', | 226 | as: 'Avatars', |
204 | required: false | 227 | required: false |
205 | } | 228 | } |
206 | ] | 229 | ] |
@@ -245,7 +268,7 @@ export type SummaryOptions = { | |||
245 | { | 268 | { |
246 | model: ActorImageModel, | 269 | model: ActorImageModel, |
247 | required: false, | 270 | required: false, |
248 | as: 'Banner' | 271 | as: 'Banners' |
249 | } | 272 | } |
250 | ] | 273 | ] |
251 | } | 274 | } |
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
474 | order: getSort(parameters.sort) | 497 | order: getSort(parameters.sort) |
475 | } | 498 | } |
476 | 499 | ||
477 | return VideoChannelModel | 500 | const getScope = (forCount: boolean) => { |
478 | .scope({ | 501 | return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] } |
479 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | 502 | } |
480 | }) | 503 | |
481 | .findAndCountAll(query) | 504 | return Promise.all([ |
482 | .then(({ rows, count }) => { | 505 | VideoChannelModel.scope(getScope(true)).count(), |
483 | return { total: count, data: rows } | 506 | VideoChannelModel.scope(getScope(false)).findAll(query) |
484 | }) | 507 | ]).then(([ total, data ]) => ({ total, data })) |
485 | } | 508 | } |
486 | 509 | ||
487 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | 510 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { |
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
519 | where | 542 | where |
520 | } | 543 | } |
521 | 544 | ||
522 | return VideoChannelModel | 545 | const getScope = (forCount: boolean) => { |
523 | .scope({ | 546 | return { |
524 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] | 547 | method: [ |
525 | }) | 548 | ScopeNames.FOR_API, { |
526 | .findAndCountAll(query) | 549 | ...pick(options, [ 'actorId', 'host', 'handles' ]), |
527 | .then(({ rows, count }) => { | 550 | |
528 | return { total: count, data: rows } | 551 | forCount |
529 | }) | 552 | } as AvailableForListOptions |
553 | ] | ||
554 | } | ||
555 | } | ||
556 | |||
557 | return Promise.all([ | ||
558 | VideoChannelModel.scope(getScope(true)).count(query), | ||
559 | VideoChannelModel.scope(getScope(false)).findAll(query) | ||
560 | ]).then(([ total, data ]) => ({ total, data })) | ||
530 | } | 561 | } |
531 | 562 | ||
532 | static listByAccountForAPI (options: { | 563 | static listByAccountForAPI (options: { |
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
552 | } | 583 | } |
553 | : null | 584 | : null |
554 | 585 | ||
555 | const query = { | 586 | const getQuery = (forCount: boolean) => { |
556 | offset: options.start, | 587 | const accountModel = forCount |
557 | limit: options.count, | 588 | ? AccountModel.unscoped() |
558 | order: getSort(options.sort), | 589 | : AccountModel |
559 | include: [ | 590 | |
560 | { | 591 | return { |
561 | model: AccountModel, | 592 | offset: options.start, |
562 | where: { | 593 | limit: options.count, |
563 | id: options.accountId | 594 | order: getSort(options.sort), |
564 | }, | 595 | include: [ |
565 | required: true | 596 | { |
566 | } | 597 | model: accountModel, |
567 | ], | 598 | where: { |
568 | where | 599 | id: options.accountId |
600 | }, | ||
601 | required: true | ||
602 | } | ||
603 | ], | ||
604 | where | ||
605 | } | ||
569 | } | 606 | } |
570 | 607 | ||
571 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] | 608 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
576 | }) | 613 | }) |
577 | } | 614 | } |
578 | 615 | ||
579 | return VideoChannelModel | 616 | return Promise.all([ |
580 | .scope(scopes) | 617 | VideoChannelModel.scope(scopes).count(getQuery(true)), |
581 | .findAndCountAll(query) | 618 | VideoChannelModel.scope(scopes).findAll(getQuery(false)) |
582 | .then(({ rows, count }) => { | 619 | ]).then(([ total, data ]) => ({ total, data })) |
583 | return { total: count, data: rows } | ||
584 | }) | ||
585 | } | 620 | } |
586 | 621 | ||
587 | static listAllByAccount (accountId: number) { | 622 | static listAllByAccount (accountId: number): Promise<MChannel[]> { |
588 | const query = { | 623 | const query = { |
589 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, | 624 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, |
590 | include: [ | 625 | include: [ |
591 | { | 626 | { |
592 | attributes: [], | 627 | attributes: [], |
593 | model: AccountModel, | 628 | model: AccountModel.unscoped(), |
594 | where: { | 629 | where: { |
595 | id: accountId | 630 | id: accountId |
596 | }, | 631 | }, |
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
621 | { | 656 | { |
622 | model: ActorImageModel, | 657 | model: ActorImageModel, |
623 | required: false, | 658 | required: false, |
624 | as: 'Banner' | 659 | as: 'Banners' |
625 | } | 660 | } |
626 | ] | 661 | ] |
627 | } | 662 | } |
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
655 | { | 690 | { |
656 | model: ActorImageModel, | 691 | model: ActorImageModel, |
657 | required: false, | 692 | required: false, |
658 | as: 'Banner' | 693 | as: 'Banners' |
659 | } | 694 | } |
660 | ] | 695 | ] |
661 | } | 696 | } |
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
685 | { | 720 | { |
686 | model: ActorImageModel, | 721 | model: ActorImageModel, |
687 | required: false, | 722 | required: false, |
688 | as: 'Banner' | 723 | as: 'Banners' |
689 | } | 724 | } |
690 | ] | 725 | ] |
691 | } | 726 | } |
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
706 | displayName: this.getDisplayName(), | 741 | displayName: this.getDisplayName(), |
707 | url: actor.url, | 742 | url: actor.url, |
708 | host: actor.host, | 743 | host: actor.host, |
744 | avatars: actor.avatars, | ||
745 | |||
746 | // TODO: remove, deprecated in 4.2 | ||
709 | avatar: actor.avatar | 747 | avatar: actor.avatar |
710 | } | 748 | } |
711 | } | 749 | } |
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
736 | support: this.support, | 774 | support: this.support, |
737 | isLocal: this.Actor.isOwned(), | 775 | isLocal: this.Actor.isOwned(), |
738 | updatedAt: this.updatedAt, | 776 | updatedAt: this.updatedAt, |
777 | |||
739 | ownerAccount: undefined, | 778 | ownerAccount: undefined, |
779 | |||
740 | videosCount, | 780 | videosCount, |
741 | viewsPerDay | 781 | viewsPerDay, |
782 | |||
783 | avatars: actor.avatars, | ||
784 | |||
785 | // TODO: remove, deprecated in 4.2 | ||
786 | avatar: actor.avatar | ||
742 | } | 787 | } |
743 | 788 | ||
744 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 789 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fa77455bc..2d60c6a30 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { uniq } from 'lodash' | 1 | import { uniq } from 'lodash' |
2 | import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -16,8 +16,8 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/typescript-utils' | ||
20 | import { VideoPrivacy } from '@shared/models' | 19 | import { VideoPrivacy } from '@shared/models' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | ||
21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' | 23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
@@ -363,40 +363,43 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | 363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) |
364 | } | 364 | } |
365 | 365 | ||
366 | const query: FindAndCountOptions = { | 366 | const getQuery = (forCount: boolean) => { |
367 | offset: start, | 367 | return { |
368 | limit: count, | 368 | offset: start, |
369 | order: getCommentSort(sort), | 369 | limit: count, |
370 | where, | 370 | order: getCommentSort(sort), |
371 | include: [ | 371 | where, |
372 | { | 372 | include: [ |
373 | model: AccountModel.unscoped(), | 373 | { |
374 | required: true, | 374 | model: AccountModel.unscoped(), |
375 | where: whereAccount, | 375 | required: true, |
376 | include: [ | 376 | where: whereAccount, |
377 | { | 377 | include: [ |
378 | attributes: { | 378 | { |
379 | exclude: unusedActorAttributesForAPI | 379 | attributes: { |
380 | }, | 380 | exclude: unusedActorAttributesForAPI |
381 | model: ActorModel, // Default scope includes avatar and server | 381 | }, |
382 | required: true, | 382 | model: forCount === true |
383 | where: whereActor | 383 | ? ActorModel.unscoped() // Default scope includes avatar and server |
384 | } | 384 | : ActorModel, |
385 | ] | 385 | required: true, |
386 | }, | 386 | where: whereActor |
387 | { | 387 | } |
388 | model: VideoModel.unscoped(), | 388 | ] |
389 | required: true, | 389 | }, |
390 | where: whereVideo | 390 | { |
391 | } | 391 | model: VideoModel.unscoped(), |
392 | ] | 392 | required: true, |
393 | where: whereVideo | ||
394 | } | ||
395 | ] | ||
396 | } | ||
393 | } | 397 | } |
394 | 398 | ||
395 | return VideoCommentModel | 399 | return Promise.all([ |
396 | .findAndCountAll(query) | 400 | VideoCommentModel.count(getQuery(true)), |
397 | .then(({ rows, count }) => { | 401 | VideoCommentModel.findAll(getQuery(false)) |
398 | return { total: count, data: rows } | 402 | ]).then(([ total, data ]) => ({ total, data })) |
399 | }) | ||
400 | } | 403 | } |
401 | 404 | ||
402 | static async listThreadsForApi (parameters: { | 405 | static async listThreadsForApi (parameters: { |
@@ -443,14 +446,20 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
443 | } | 446 | } |
444 | } | 447 | } |
445 | 448 | ||
446 | const scopesList: (string | ScopeOptions)[] = [ | 449 | const findScopesList: (string | ScopeOptions)[] = [ |
447 | ScopeNames.WITH_ACCOUNT_FOR_API, | 450 | ScopeNames.WITH_ACCOUNT_FOR_API, |
448 | { | 451 | { |
449 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 452 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] |
450 | } | 453 | } |
451 | ] | 454 | ] |
452 | 455 | ||
453 | const queryCount = { | 456 | const countScopesList: ScopeOptions[] = [ |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | |||
462 | const notDeletedQueryCount = { | ||
454 | where: { | 463 | where: { |
455 | videoId, | 464 | videoId, |
456 | deletedAt: null, | 465 | deletedAt: null, |
@@ -459,9 +468,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
459 | } | 468 | } |
460 | 469 | ||
461 | return Promise.all([ | 470 | return Promise.all([ |
462 | VideoCommentModel.scope(scopesList).findAndCountAll(queryList), | 471 | VideoCommentModel.scope(findScopesList).findAll(queryList), |
463 | VideoCommentModel.count(queryCount) | 472 | VideoCommentModel.scope(countScopesList).count(queryList), |
464 | ]).then(([ { rows, count }, totalNotDeletedComments ]) => { | 473 | VideoCommentModel.count(notDeletedQueryCount) |
474 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | ||
465 | return { total: count, data: rows, totalNotDeletedComments } | 475 | return { total: count, data: rows, totalNotDeletedComments } |
466 | }) | 476 | }) |
467 | } | 477 | } |
@@ -512,11 +522,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
512 | } | 522 | } |
513 | ] | 523 | ] |
514 | 524 | ||
515 | return VideoCommentModel.scope(scopes) | 525 | return Promise.all([ |
516 | .findAndCountAll(query) | 526 | VideoCommentModel.count(query), |
517 | .then(({ rows, count }) => { | 527 | VideoCommentModel.scope(scopes).findAll(query) |
518 | return { total: count, data: rows } | 528 | ]).then(([ total, data ]) => ({ total, data })) |
519 | }) | ||
520 | } | 529 | } |
521 | 530 | ||
522 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 531 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -565,7 +574,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
565 | transaction: t | 574 | transaction: t |
566 | } | 575 | } |
567 | 576 | ||
568 | return VideoCommentModel.findAndCountAll<MComment>(query) | 577 | return Promise.all([ |
578 | VideoCommentModel.count(query), | ||
579 | VideoCommentModel.findAll<MComment>(query) | ||
580 | ]).then(([ total, data ]) => ({ total, data })) | ||
569 | } | 581 | } |
570 | 582 | ||
571 | static async listForFeed (parameters: { | 583 | static async listForFeed (parameters: { |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 5d2b230e8..1d8296060 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -155,13 +155,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo | |||
155 | where | 155 | where |
156 | } | 156 | } |
157 | 157 | ||
158 | return VideoImportModel.findAndCountAll<MVideoImportDefault>(query) | 158 | return Promise.all([ |
159 | .then(({ rows, count }) => { | 159 | VideoImportModel.unscoped().count(query), |
160 | return { | 160 | VideoImportModel.findAll<MVideoImportDefault>(query) |
161 | data: rows, | 161 | ]).then(([ total, data ]) => ({ total, data })) |
162 | total: count | ||
163 | } | ||
164 | }) | ||
165 | } | 162 | } |
166 | 163 | ||
167 | getTargetIdentifier () { | 164 | getTargetIdentifier () { |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index e20e32f8b..4e4160818 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -23,6 +23,7 @@ import { | |||
23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, | 23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, |
24 | MVideoPlaylistVideoThumbnail | 24 | MVideoPlaylistVideoThumbnail |
25 | } from '@server/types/models/video/video-playlist-element' | 25 | } from '@server/types/models/video/video-playlist-element' |
26 | import { AttributesOnly } from '@shared/typescript-utils' | ||
26 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 27 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
27 | import { VideoPrivacy } from '../../../shared/models/videos' | 28 | import { VideoPrivacy } from '../../../shared/models/videos' |
28 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' | 29 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' |
@@ -32,7 +33,6 @@ import { AccountModel } from '../account/account' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 33 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 34 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
34 | import { VideoPlaylistModel } from './video-playlist' | 35 | import { VideoPlaylistModel } from './video-playlist' |
35 | import { AttributesOnly } from '@shared/typescript-utils' | ||
36 | 36 | ||
37 | @Table({ | 37 | @Table({ |
38 | tableName: 'videoPlaylistElement', | 38 | tableName: 'videoPlaylistElement', |
@@ -208,22 +208,28 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
208 | } | 208 | } |
209 | 209 | ||
210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { | 210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { |
211 | const query = { | 211 | const getQuery = (forCount: boolean) => { |
212 | attributes: [ 'url' ], | 212 | return { |
213 | offset: start, | 213 | attributes: forCount |
214 | limit: count, | 214 | ? [] |
215 | order: getSort('position'), | 215 | : [ 'url' ], |
216 | where: { | 216 | offset: start, |
217 | videoPlaylistId | 217 | limit: count, |
218 | }, | 218 | order: getSort('position'), |
219 | transaction: t | 219 | where: { |
220 | videoPlaylistId | ||
221 | }, | ||
222 | transaction: t | ||
223 | } | ||
220 | } | 224 | } |
221 | 225 | ||
222 | return VideoPlaylistElementModel | 226 | return Promise.all([ |
223 | .findAndCountAll(query) | 227 | VideoPlaylistElementModel.count(getQuery(true)), |
224 | .then(({ rows, count }) => { | 228 | VideoPlaylistElementModel.findAll(getQuery(false)) |
225 | return { total: count, data: rows.map(e => e.url) } | 229 | ]).then(([ total, rows ]) => ({ |
226 | }) | 230 | total, |
231 | data: rows.map(e => e.url) | ||
232 | })) | ||
227 | } | 233 | } |
228 | 234 | ||
229 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { | 235 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index c125db3ff..ae5e237ec 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -86,6 +86,7 @@ type AvailableForListOptions = { | |||
86 | host?: string | 86 | host?: string |
87 | uuids?: string[] | 87 | uuids?: string[] |
88 | withVideos?: boolean | 88 | withVideos?: boolean |
89 | forCount?: boolean | ||
89 | } | 90 | } |
90 | 91 | ||
91 | function getVideoLengthSelect () { | 92 | function getVideoLengthSelect () { |
@@ -239,23 +240,28 @@ function getVideoLengthSelect () { | |||
239 | [Op.and]: whereAnd | 240 | [Op.and]: whereAnd |
240 | } | 241 | } |
241 | 242 | ||
243 | const include: Includeable[] = [ | ||
244 | { | ||
245 | model: AccountModel.scope({ | ||
246 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ] | ||
247 | }), | ||
248 | required: true | ||
249 | } | ||
250 | ] | ||
251 | |||
252 | if (options.forCount !== true) { | ||
253 | include.push({ | ||
254 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
255 | required: false | ||
256 | }) | ||
257 | } | ||
258 | |||
242 | return { | 259 | return { |
243 | attributes: { | 260 | attributes: { |
244 | include: attributesInclude | 261 | include: attributesInclude |
245 | }, | 262 | }, |
246 | where, | 263 | where, |
247 | include: [ | 264 | include |
248 | { | ||
249 | model: AccountModel.scope({ | ||
250 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] | ||
251 | }), | ||
252 | required: true | ||
253 | }, | ||
254 | { | ||
255 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
256 | required: false | ||
257 | } | ||
258 | ] | ||
259 | } as FindOptions | 265 | } as FindOptions |
260 | } | 266 | } |
261 | })) | 267 | })) |
@@ -369,12 +375,23 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
369 | order: getPlaylistSort(options.sort) | 375 | order: getPlaylistSort(options.sort) |
370 | } | 376 | } |
371 | 377 | ||
372 | const scopes: (string | ScopeOptions)[] = [ | 378 | const commonAvailableForListOptions = pick(options, [ |
379 | 'type', | ||
380 | 'followerActorId', | ||
381 | 'accountId', | ||
382 | 'videoChannelId', | ||
383 | 'listMyPlaylists', | ||
384 | 'search', | ||
385 | 'host', | ||
386 | 'uuids' | ||
387 | ]) | ||
388 | |||
389 | const scopesFind: (string | ScopeOptions)[] = [ | ||
373 | { | 390 | { |
374 | method: [ | 391 | method: [ |
375 | ScopeNames.AVAILABLE_FOR_LIST, | 392 | ScopeNames.AVAILABLE_FOR_LIST, |
376 | { | 393 | { |
377 | ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), | 394 | ...commonAvailableForListOptions, |
378 | 395 | ||
379 | withVideos: options.withVideos || false | 396 | withVideos: options.withVideos || false |
380 | } as AvailableForListOptions | 397 | } as AvailableForListOptions |
@@ -384,12 +401,26 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
384 | ScopeNames.WITH_THUMBNAIL | 401 | ScopeNames.WITH_THUMBNAIL |
385 | ] | 402 | ] |
386 | 403 | ||
387 | return VideoPlaylistModel | 404 | const scopesCount: (string | ScopeOptions)[] = [ |
388 | .scope(scopes) | 405 | { |
389 | .findAndCountAll(query) | 406 | method: [ |
390 | .then(({ rows, count }) => { | 407 | ScopeNames.AVAILABLE_FOR_LIST, |
391 | return { total: count, data: rows } | 408 | |
392 | }) | 409 | { |
410 | ...commonAvailableForListOptions, | ||
411 | |||
412 | withVideos: options.withVideos || false, | ||
413 | forCount: true | ||
414 | } as AvailableForListOptions | ||
415 | ] | ||
416 | }, | ||
417 | ScopeNames.WITH_VIDEOS_LENGTH | ||
418 | ] | ||
419 | |||
420 | return Promise.all([ | ||
421 | VideoPlaylistModel.scope(scopesCount).count(), | ||
422 | VideoPlaylistModel.scope(scopesFind).findAll(query) | ||
423 | ]).then(([ count, rows ]) => ({ total: count, data: rows })) | ||
393 | } | 424 | } |
394 | 425 | ||
395 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { | 426 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { |
@@ -419,17 +450,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
419 | Object.assign(where, { videoChannelId: options.channel.id }) | 450 | Object.assign(where, { videoChannelId: options.channel.id }) |
420 | } | 451 | } |
421 | 452 | ||
422 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
423 | attributes: [ 'url' ], | 454 | return { |
424 | offset: start, | 455 | attributes: forCount === true |
425 | limit: count, | 456 | ? [] |
426 | where | 457 | : [ 'url' ], |
458 | offset: start, | ||
459 | limit: count, | ||
460 | where | ||
461 | } | ||
427 | } | 462 | } |
428 | 463 | ||
429 | return VideoPlaylistModel.findAndCountAll(query) | 464 | return Promise.all([ |
430 | .then(({ rows, count }) => { | 465 | VideoPlaylistModel.count(getQuery(true)), |
431 | return { total: count, data: rows.map(p => p.url) } | 466 | VideoPlaylistModel.findAll(getQuery(false)) |
432 | }) | 467 | ]).then(([ total, rows ]) => ({ |
468 | total, | ||
469 | data: rows.map(p => p.url) | ||
470 | })) | ||
433 | } | 471 | } |
434 | 472 | ||
435 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { | 473 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f6659b992..ad95dec6e 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -183,7 +183,10 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode | |||
183 | transaction: t | 183 | transaction: t |
184 | } | 184 | } |
185 | 185 | ||
186 | return VideoShareModel.findAndCountAll(query) | 186 | return Promise.all([ |
187 | VideoShareModel.count(query), | ||
188 | VideoShareModel.findAll(query) | ||
189 | ]).then(([ total, data ]) => ({ total, data })) | ||
187 | } | 190 | } |
188 | 191 | ||
189 | static listRemoteShareUrlsOfLocalVideos () { | 192 | static listRemoteShareUrlsOfLocalVideos () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 9111c71b0..5536334eb 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -114,9 +114,13 @@ import { | |||
114 | videoModelToFormattedJSON | 114 | videoModelToFormattedJSON |
115 | } from './formatter/video-format-utils' | 115 | } from './formatter/video-format-utils' |
116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
117 | import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder' | 117 | import { |
118 | import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | 118 | BuildVideosListQueryOptions, |
119 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | 119 | DisplayOnlyForFollowerOptions, |
120 | VideoModelGetQueryBuilder, | ||
121 | VideosIdListQueryBuilder, | ||
122 | VideosModelListQueryBuilder | ||
123 | } from './sql/video' | ||
120 | import { TagModel } from './tag' | 124 | import { TagModel } from './tag' |
121 | import { ThumbnailModel } from './thumbnail' | 125 | import { ThumbnailModel } from './thumbnail' |
122 | import { VideoBlacklistModel } from './video-blacklist' | 126 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -229,8 +233,8 @@ export type ForAPIOptions = { | |||
229 | required: false | 233 | required: false |
230 | }, | 234 | }, |
231 | { | 235 | { |
232 | model: ActorImageModel.unscoped(), | 236 | model: ActorImageModel, |
233 | as: 'Avatar', | 237 | as: 'Avatars', |
234 | required: false | 238 | required: false |
235 | } | 239 | } |
236 | ] | 240 | ] |
@@ -252,8 +256,8 @@ export type ForAPIOptions = { | |||
252 | required: false | 256 | required: false |
253 | }, | 257 | }, |
254 | { | 258 | { |
255 | model: ActorImageModel.unscoped(), | 259 | model: ActorImageModel, |
256 | as: 'Avatar', | 260 | as: 'Avatars', |
257 | required: false | 261 | required: false |
258 | } | 262 | } |
259 | ] | 263 | ] |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 1e9732fe9..5c2650fac 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -228,7 +228,7 @@ describe('Test video channels API validator', function () { | |||
228 | }) | 228 | }) |
229 | }) | 229 | }) |
230 | 230 | ||
231 | describe('When updating video channel avatar/banner', function () { | 231 | describe('When updating video channel avatars/banners', function () { |
232 | const types = [ 'avatar', 'banner' ] | 232 | const types = [ 'avatar', 'banner' ] |
233 | let path: string | 233 | let path: string |
234 | 234 | ||
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts index 0c3bed3e7..7bf49c7ec 100644 --- a/server/tests/api/moderation/abuses.ts +++ b/server/tests/api/moderation/abuses.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | AbusesCommand, | 7 | AbusesCommand, |
7 | cleanupTests, | 8 | cleanupTests, |
@@ -9,9 +10,10 @@ import { | |||
9 | doubleFollow, | 10 | doubleFollow, |
10 | PeerTubeServer, | 11 | PeerTubeServer, |
11 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
12 | waitJobs | 15 | waitJobs |
13 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
14 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@shared/models' | ||
15 | 17 | ||
16 | const expect = chai.expect | 18 | const expect = chai.expect |
17 | 19 | ||
@@ -27,8 +29,9 @@ describe('Test abuses', function () { | |||
27 | // Run servers | 29 | // Run servers |
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | // Get the access tokens | ||
31 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultChannelAvatar(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
32 | 35 | ||
33 | // Server 1 and server 2 follow each other | 36 | // Server 1 and server 2 follow each other |
34 | await doubleFollow(servers[0], servers[1]) | 37 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts index b45460bb4..e1344a245 100644 --- a/server/tests/api/moderation/blocklist.ts +++ b/server/tests/api/moderation/blocklist.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { UserNotificationType } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | BlocklistCommand, | 7 | BlocklistCommand, |
7 | cleanupTests, | 8 | cleanupTests, |
@@ -10,9 +11,9 @@ import { | |||
10 | doubleFollow, | 11 | doubleFollow, |
11 | PeerTubeServer, | 12 | PeerTubeServer, |
12 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | import { UserNotificationType } from '@shared/models' | ||
16 | 17 | ||
17 | const expect = chai.expect | 18 | const expect = chai.expect |
18 | 19 | ||
@@ -79,6 +80,7 @@ describe('Test blocklist', function () { | |||
79 | 80 | ||
80 | servers = await createMultipleServers(3) | 81 | servers = await createMultipleServers(3) |
81 | await setAccessTokensToServers(servers) | 82 | await setAccessTokensToServers(servers) |
83 | await setDefaultAccountAvatar(servers) | ||
82 | 84 | ||
83 | command = servers[0].blocklist | 85 | command = servers[0].blocklist |
84 | commentsCommand = servers.map(s => s.comments) | 86 | commentsCommand = servers.map(s => s.comments) |
diff --git a/server/tests/api/moderation/video-blacklist.ts b/server/tests/api/moderation/video-blacklist.ts index 3e7f2ba33..1790210ff 100644 --- a/server/tests/api/moderation/video-blacklist.ts +++ b/server/tests/api/moderation/video-blacklist.ts | |||
@@ -13,6 +13,7 @@ import { | |||
13 | killallServers, | 13 | killallServers, |
14 | PeerTubeServer, | 14 | PeerTubeServer, |
15 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
16 | setDefaultChannelAvatar, | ||
16 | waitJobs | 17 | waitJobs |
17 | } from '@shared/server-commands' | 18 | } from '@shared/server-commands' |
18 | 19 | ||
@@ -42,6 +43,7 @@ describe('Test video blacklist', function () { | |||
42 | 43 | ||
43 | // Server 1 and server 2 follow each other | 44 | // Server 1 and server 2 follow each other |
44 | await doubleFollow(servers[0], servers[1]) | 45 | await doubleFollow(servers[0], servers[1]) |
46 | await setDefaultChannelAvatar(servers[0]) | ||
45 | 47 | ||
46 | // Upload 2 videos on server 2 | 48 | // Upload 2 videos on server 2 |
47 | await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } }) | 49 | await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } }) |
diff --git a/server/tests/api/notifications/notifications-api.ts b/server/tests/api/notifications/notifications-api.ts index ac08449f8..78864c8a0 100644 --- a/server/tests/api/notifications/notifications-api.ts +++ b/server/tests/api/notifications/notifications-api.ts | |||
@@ -38,6 +38,16 @@ describe('Test notifications API', function () { | |||
38 | await waitJobs([ server ]) | 38 | await waitJobs([ server ]) |
39 | }) | 39 | }) |
40 | 40 | ||
41 | describe('Notification list & count', function () { | ||
42 | |||
43 | it('Should correctly list notifications', async function () { | ||
44 | const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 }) | ||
45 | |||
46 | expect(data).to.have.lengthOf(2) | ||
47 | expect(total).to.equal(10) | ||
48 | }) | ||
49 | }) | ||
50 | |||
41 | describe('Mark as read', function () { | 51 | describe('Mark as read', function () { |
42 | 52 | ||
43 | it('Should mark as read some notifications', async function () { | 53 | it('Should mark as read some notifications', async function () { |
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index 2e0abc6ba..5f5322d03 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -10,6 +10,8 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultVideoChannel, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | 17 | ||
@@ -28,6 +30,8 @@ describe('Test ActivityPub video channels search', function () { | |||
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
31 | 35 | ||
32 | { | 36 | { |
33 | await servers[0].users.create({ username: 'user1_server1', password: 'password' }) | 37 | await servers[0].users.create({ username: 'user1_server1', password: 'password' }) |
diff --git a/server/tests/api/search/search-activitypub-video-playlists.ts b/server/tests/api/search/search-activitypub-video-playlists.ts index d9243ac53..b9a424292 100644 --- a/server/tests/api/search/search-activitypub-video-playlists.ts +++ b/server/tests/api/search/search-activitypub-video-playlists.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | 14 | setDefaultVideoChannel, |
14 | waitJobs | 15 | waitJobs |
15 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
@@ -31,6 +32,7 @@ describe('Test ActivityPub playlists search', function () { | |||
31 | 32 | ||
32 | await setAccessTokensToServers(servers) | 33 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | 34 | await setDefaultVideoChannel(servers) |
35 | await setDefaultAccountAvatar(servers) | ||
34 | 36 | ||
35 | { | 37 | { |
36 | const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid | 38 | const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid |
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index 60b95ae4c..20249b1f1 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts | |||
@@ -10,6 +10,8 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultVideoChannel, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | 17 | ||
@@ -28,6 +30,8 @@ describe('Test ActivityPub videos search', function () { | |||
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
31 | 35 | ||
32 | { | 36 | { |
33 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } }) | 37 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } }) |
diff --git a/server/tests/api/search/search-channels.ts b/server/tests/api/search/search-channels.ts index 8a92def61..0073c71e1 100644 --- a/server/tests/api/search/search-channels.ts +++ b/server/tests/api/search/search-channels.ts | |||
@@ -2,15 +2,17 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoChannel } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createSingleServer, | 8 | createSingleServer, |
8 | doubleFollow, | 9 | doubleFollow, |
9 | PeerTubeServer, | 10 | PeerTubeServer, |
10 | SearchCommand, | 11 | SearchCommand, |
11 | setAccessTokensToServers | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | 15 | } from '@shared/server-commands' |
13 | import { VideoChannel } from '@shared/models' | ||
14 | 16 | ||
15 | const expect = chai.expect | 17 | const expect = chai.expect |
16 | 18 | ||
@@ -30,6 +32,8 @@ describe('Test channels search', function () { | |||
30 | remoteServer = servers[1] | 32 | remoteServer = servers[1] |
31 | 33 | ||
32 | await setAccessTokensToServers([ server, remoteServer ]) | 34 | await setAccessTokensToServers([ server, remoteServer ]) |
35 | await setDefaultChannelAvatar(server) | ||
36 | await setDefaultAccountAvatar(server) | ||
33 | 37 | ||
34 | { | 38 | { |
35 | await server.users.create({ username: 'user1' }) | 39 | await server.users.create({ username: 'user1' }) |
diff --git a/server/tests/api/search/search-index.ts b/server/tests/api/search/search-index.ts index f84d03345..ae933449f 100644 --- a/server/tests/api/search/search-index.ts +++ b/server/tests/api/search/search-index.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | 14 | ||
15 | const expect = chai.expect | 15 | const expect = chai.expect |
16 | 16 | ||
17 | describe('Test videos search', function () { | 17 | describe('Test index search', function () { |
18 | const localVideoName = 'local video' + new Date().toISOString() | 18 | const localVideoName = 'local video' + new Date().toISOString() |
19 | 19 | ||
20 | let server: PeerTubeServer = null | 20 | let server: PeerTubeServer = null |
@@ -134,12 +134,16 @@ describe('Test videos search', function () { | |||
134 | expect(video.account.host).to.equal('framatube.org') | 134 | expect(video.account.host).to.equal('framatube.org') |
135 | expect(video.account.name).to.equal('framasoft') | 135 | expect(video.account.name).to.equal('framasoft') |
136 | expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') | 136 | expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') |
137 | // TODO: remove, deprecated in 4.2 | ||
137 | expect(video.account.avatar).to.exist | 138 | expect(video.account.avatar).to.exist |
139 | expect(video.account.avatars.length).to.equal(1, 'Account should have one avatar image') | ||
138 | 140 | ||
139 | expect(video.channel.host).to.equal('framatube.org') | 141 | expect(video.channel.host).to.equal('framatube.org') |
140 | expect(video.channel.name).to.equal('joinpeertube') | 142 | expect(video.channel.name).to.equal('joinpeertube') |
141 | expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') | 143 | expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') |
144 | // TODO: remove, deprecated in 4.2 | ||
142 | expect(video.channel.avatar).to.exist | 145 | expect(video.channel.avatar).to.exist |
146 | expect(video.channel.avatars.length).to.equal(1, 'Channel should have one avatar image') | ||
143 | } | 147 | } |
144 | 148 | ||
145 | const baseSearch: VideosSearchQuery = { | 149 | const baseSearch: VideosSearchQuery = { |
@@ -316,13 +320,17 @@ describe('Test videos search', function () { | |||
316 | const videoChannel = body.data[0] | 320 | const videoChannel = body.data[0] |
317 | expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8') | 321 | expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8') |
318 | expect(videoChannel.host).to.equal('framatube.org') | 322 | expect(videoChannel.host).to.equal('framatube.org') |
323 | // TODO: remove, deprecated in 4.2 | ||
319 | expect(videoChannel.avatar).to.exist | 324 | expect(videoChannel.avatar).to.exist |
325 | expect(videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') | ||
320 | expect(videoChannel.displayName).to.exist | 326 | expect(videoChannel.displayName).to.exist |
321 | 327 | ||
322 | expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') | 328 | expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') |
323 | expect(videoChannel.ownerAccount.name).to.equal('framasoft') | 329 | expect(videoChannel.ownerAccount.name).to.equal('framasoft') |
324 | expect(videoChannel.ownerAccount.host).to.equal('framatube.org') | 330 | expect(videoChannel.ownerAccount.host).to.equal('framatube.org') |
331 | // TODO: remove, deprecated in 4.2 | ||
325 | expect(videoChannel.ownerAccount.avatar).to.exist | 332 | expect(videoChannel.ownerAccount.avatar).to.exist |
333 | expect(videoChannel.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') | ||
326 | } | 334 | } |
327 | 335 | ||
328 | it('Should make a simple search and not have results', async function () { | 336 | it('Should make a simple search and not have results', async function () { |
@@ -388,12 +396,16 @@ describe('Test videos search', function () { | |||
388 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') | 396 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') |
389 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') | 397 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') |
390 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') | 398 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') |
399 | // TODO: remove, deprecated in 4.2 | ||
391 | expect(videoPlaylist.ownerAccount.avatar).to.exist | 400 | expect(videoPlaylist.ownerAccount.avatar).to.exist |
401 | expect(videoPlaylist.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') | ||
392 | 402 | ||
393 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') | 403 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') |
394 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') | 404 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') |
395 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') | 405 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') |
406 | // TODO: remove, deprecated in 4.2 | ||
396 | expect(videoPlaylist.videoChannel.avatar).to.exist | 407 | expect(videoPlaylist.videoChannel.avatar).to.exist |
408 | expect(videoPlaylist.videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') | ||
397 | } | 409 | } |
398 | 410 | ||
399 | it('Should make a simple search and not have results', async function () { | 411 | it('Should make a simple search and not have results', async function () { |
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts index 1e9c8d4bb..fcf2f2ee2 100644 --- a/server/tests/api/search/search-playlists.ts +++ b/server/tests/api/search/search-playlists.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoPlaylistPrivacy } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createSingleServer, | 8 | createSingleServer, |
@@ -9,9 +10,10 @@ import { | |||
9 | PeerTubeServer, | 10 | PeerTubeServer, |
10 | SearchCommand, | 11 | SearchCommand, |
11 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
12 | setDefaultVideoChannel | 15 | setDefaultVideoChannel |
13 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
14 | import { VideoPlaylistPrivacy } from '@shared/models' | ||
15 | 17 | ||
16 | const expect = chai.expect | 18 | const expect = chai.expect |
17 | 19 | ||
@@ -34,6 +36,8 @@ describe('Test playlists search', function () { | |||
34 | 36 | ||
35 | await setAccessTokensToServers([ remoteServer, server ]) | 37 | await setAccessTokensToServers([ remoteServer, server ]) |
36 | await setDefaultVideoChannel([ remoteServer, server ]) | 38 | await setDefaultVideoChannel([ remoteServer, server ]) |
39 | await setDefaultChannelAvatar([ remoteServer, server ]) | ||
40 | await setDefaultAccountAvatar([ remoteServer, server ]) | ||
37 | 41 | ||
38 | { | 42 | { |
39 | const videoId = (await server.videos.upload()).uuid | 43 | const videoId = (await server.videos.upload()).uuid |
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index c544705d3..ff4c3c161 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts | |||
@@ -2,6 +2,8 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { wait } from '@shared/core-utils' | ||
6 | import { VideoPrivacy } from '@shared/models' | ||
5 | import { | 7 | import { |
6 | cleanupTests, | 8 | cleanupTests, |
7 | createSingleServer, | 9 | createSingleServer, |
@@ -9,11 +11,11 @@ import { | |||
9 | PeerTubeServer, | 11 | PeerTubeServer, |
10 | SearchCommand, | 12 | SearchCommand, |
11 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
15 | setDefaultChannelAvatar, | ||
12 | setDefaultVideoChannel, | 16 | setDefaultVideoChannel, |
13 | stopFfmpeg | 17 | stopFfmpeg |
14 | } from '@shared/server-commands' | 18 | } from '@shared/server-commands' |
15 | import { VideoPrivacy } from '@shared/models' | ||
16 | import { wait } from '@shared/core-utils' | ||
17 | 19 | ||
18 | const expect = chai.expect | 20 | const expect = chai.expect |
19 | 21 | ||
@@ -38,6 +40,8 @@ describe('Test videos search', function () { | |||
38 | 40 | ||
39 | await setAccessTokensToServers([ server, remoteServer ]) | 41 | await setAccessTokensToServers([ server, remoteServer ]) |
40 | await setDefaultVideoChannel([ server, remoteServer ]) | 42 | await setDefaultVideoChannel([ server, remoteServer ]) |
43 | await setDefaultChannelAvatar(server) | ||
44 | await setDefaultAccountAvatar(servers) | ||
41 | 45 | ||
42 | { | 46 | { |
43 | const attributes1 = { | 47 | const attributes1 = { |
diff --git a/server/tests/api/server/homepage.ts b/server/tests/api/server/homepage.ts index 552ee98cf..e7de6bfee 100644 --- a/server/tests/api/server/homepage.ts +++ b/server/tests/api/server/homepage.ts | |||
@@ -9,7 +9,9 @@ import { | |||
9 | CustomPagesCommand, | 9 | CustomPagesCommand, |
10 | killallServers, | 10 | killallServers, |
11 | PeerTubeServer, | 11 | PeerTubeServer, |
12 | setAccessTokensToServers | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
13 | } from '../../../../shared/server-commands/index' | 15 | } from '../../../../shared/server-commands/index' |
14 | 16 | ||
15 | const expect = chai.expect | 17 | const expect = chai.expect |
@@ -29,6 +31,8 @@ describe('Test instance homepage actions', function () { | |||
29 | 31 | ||
30 | server = await createSingleServer(1) | 32 | server = await createSingleServer(1) |
31 | await setAccessTokensToServers([ server ]) | 33 | await setAccessTokensToServers([ server ]) |
34 | await setDefaultChannelAvatar(server) | ||
35 | await setDefaultAccountAvatar(server) | ||
32 | 36 | ||
33 | command = server.customPage | 37 | command = server.customPage |
34 | }) | 38 | }) |
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts index 57cca6ad4..9553a69bb 100644 --- a/server/tests/api/users/user-subscriptions.ts +++ b/server/tests/api/users/user-subscriptions.ts | |||
@@ -9,6 +9,8 @@ import { | |||
9 | doubleFollow, | 9 | doubleFollow, |
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
12 | SubscriptionsCommand, | 14 | SubscriptionsCommand, |
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
@@ -29,6 +31,8 @@ describe('Test users subscriptions', function () { | |||
29 | 31 | ||
30 | // Get the access tokens | 32 | // Get the access tokens |
31 | await setAccessTokensToServers(servers) | 33 | await setAccessTokensToServers(servers) |
34 | await setDefaultChannelAvatar(servers) | ||
35 | await setDefaultAccountAvatar(servers) | ||
32 | 36 | ||
33 | // Server 1 and server 2 follow each other | 37 | // Server 1 and server 2 follow each other |
34 | await doubleFollow(servers[0], servers[1]) | 38 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index 5b2bbc520..3e8b932c0 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -16,6 +16,7 @@ import { | |||
16 | doubleFollow, | 16 | doubleFollow, |
17 | PeerTubeServer, | 17 | PeerTubeServer, |
18 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
19 | setDefaultChannelAvatar, | ||
19 | waitJobs | 20 | waitJobs |
20 | } from '@shared/server-commands' | 21 | } from '@shared/server-commands' |
21 | 22 | ||
@@ -29,7 +30,7 @@ describe('Test users with multiple servers', function () { | |||
29 | 30 | ||
30 | let videoUUID: string | 31 | let videoUUID: string |
31 | let userAccessToken: string | 32 | let userAccessToken: string |
32 | let userAvatarFilename: string | 33 | let userAvatarFilenames: string[] |
33 | 34 | ||
34 | before(async function () { | 35 | before(async function () { |
35 | this.timeout(120_000) | 36 | this.timeout(120_000) |
@@ -38,6 +39,7 @@ describe('Test users with multiple servers', function () { | |||
38 | 39 | ||
39 | // Get the access tokens | 40 | // Get the access tokens |
40 | await setAccessTokensToServers(servers) | 41 | await setAccessTokensToServers(servers) |
42 | await setDefaultChannelAvatar(servers) | ||
41 | 43 | ||
42 | // Server 1 and server 2 follow each other | 44 | // Server 1 and server 2 follow each other |
43 | await doubleFollow(servers[0], servers[1]) | 45 | await doubleFollow(servers[0], servers[1]) |
@@ -97,9 +99,11 @@ describe('Test users with multiple servers', function () { | |||
97 | await servers[0].users.updateMyAvatar({ fixture }) | 99 | await servers[0].users.updateMyAvatar({ fixture }) |
98 | 100 | ||
99 | user = await servers[0].users.getMyInfo() | 101 | user = await servers[0].users.getMyInfo() |
100 | userAvatarFilename = user.account.avatar.path | 102 | userAvatarFilenames = user.account.avatars.map(({ path }) => path) |
101 | 103 | ||
102 | await testImage(servers[0].url, 'avatar2-resized', userAvatarFilename, '.png') | 104 | for (const avatar of user.account.avatars) { |
105 | await testImage(servers[0].url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
106 | } | ||
103 | 107 | ||
104 | await waitJobs(servers) | 108 | await waitJobs(servers) |
105 | }) | 109 | }) |
@@ -129,7 +133,9 @@ describe('Test users with multiple servers', function () { | |||
129 | expect(account.userId).to.be.undefined | 133 | expect(account.userId).to.be.undefined |
130 | } | 134 | } |
131 | 135 | ||
132 | await testImage(server.url, 'avatar2-resized', account.avatar.path, '.png') | 136 | for (const avatar of account.avatars) { |
137 | await testImage(server.url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
138 | } | ||
133 | } | 139 | } |
134 | }) | 140 | }) |
135 | 141 | ||
@@ -193,7 +199,9 @@ describe('Test users with multiple servers', function () { | |||
193 | 199 | ||
194 | it('Should not have actor files', async () => { | 200 | it('Should not have actor files', async () => { |
195 | for (const server of servers) { | 201 | for (const server of servers) { |
196 | await checkActorFilesWereRemoved(userAvatarFilename, server.internalServerNumber) | 202 | for (const userAvatarFilename of userAvatarFilenames) { |
203 | await checkActorFilesWereRemoved(userAvatarFilename, server.internalServerNumber) | ||
204 | } | ||
197 | } | 205 | } |
198 | }) | 206 | }) |
199 | 207 | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 7023b3f08..a47713bf0 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -604,7 +604,9 @@ describe('Test users', function () { | |||
604 | await server.users.updateMyAvatar({ token: userToken, fixture }) | 604 | await server.users.updateMyAvatar({ token: userToken, fixture }) |
605 | 605 | ||
606 | const user = await server.users.getMyInfo({ token: userToken }) | 606 | const user = await server.users.getMyInfo({ token: userToken }) |
607 | await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.gif') | 607 | for (const avatar of user.account.avatars) { |
608 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.gif') | ||
609 | } | ||
608 | }) | 610 | }) |
609 | 611 | ||
610 | it('Should be able to update my avatar with a gif, and then a png', async function () { | 612 | it('Should be able to update my avatar with a gif, and then a png', async function () { |
@@ -614,7 +616,9 @@ describe('Test users', function () { | |||
614 | await server.users.updateMyAvatar({ token: userToken, fixture }) | 616 | await server.users.updateMyAvatar({ token: userToken, fixture }) |
615 | 617 | ||
616 | const user = await server.users.getMyInfo({ token: userToken }) | 618 | const user = await server.users.getMyInfo({ token: userToken }) |
617 | await testImage(server.url, 'avatar-resized', user.account.avatar.path, extension) | 619 | for (const avatar of user.account.avatars) { |
620 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, extension) | ||
621 | } | ||
618 | } | 622 | } |
619 | }) | 623 | }) |
620 | 624 | ||
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index ecdd36613..5bbc60559 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -19,6 +19,8 @@ import { | |||
19 | doubleFollow, | 19 | doubleFollow, |
20 | PeerTubeServer, | 20 | PeerTubeServer, |
21 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
22 | setDefaultAccountAvatar, | ||
23 | setDefaultChannelAvatar, | ||
22 | waitJobs, | 24 | waitJobs, |
23 | webtorrentAdd | 25 | webtorrentAdd |
24 | } from '@shared/server-commands' | 26 | } from '@shared/server-commands' |
@@ -46,6 +48,9 @@ describe('Test multiple servers', function () { | |||
46 | description: 'super channel' | 48 | description: 'super channel' |
47 | } | 49 | } |
48 | await servers[0].channels.create({ attributes: videoChannel }) | 50 | await servers[0].channels.create({ attributes: videoChannel }) |
51 | await setDefaultChannelAvatar(servers[0], videoChannel.name) | ||
52 | await setDefaultAccountAvatar(servers) | ||
53 | |||
49 | const { data } = await servers[0].channels.list({ start: 0, count: 1 }) | 54 | const { data } = await servers[0].channels.list({ start: 0, count: 1 }) |
50 | videoChannelId = data[0].id | 55 | videoChannelId = data[0].id |
51 | } | 56 | } |
@@ -207,7 +212,7 @@ describe('Test multiple servers', function () { | |||
207 | }, | 212 | }, |
208 | { | 213 | { |
209 | resolution: 720, | 214 | resolution: 720, |
210 | size: 788000 | 215 | size: 750000 |
211 | } | 216 | } |
212 | ], | 217 | ], |
213 | thumbnailfile: 'thumbnail', | 218 | thumbnailfile: 'thumbnail', |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 28bf018c5..d37043aef 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -5,7 +5,14 @@ import * as chai from 'chai' | |||
5 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' | 5 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' |
6 | import { wait } from '@shared/core-utils' | 6 | import { wait } from '@shared/core-utils' |
7 | import { Video, VideoPrivacy } from '@shared/models' | 7 | import { Video, VideoPrivacy } from '@shared/models' |
8 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 8 | import { |
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
15 | } from '@shared/server-commands' | ||
9 | 16 | ||
10 | const expect = chai.expect | 17 | const expect = chai.expect |
11 | 18 | ||
@@ -90,6 +97,8 @@ describe('Test a single server', function () { | |||
90 | server = await createSingleServer(1) | 97 | server = await createSingleServer(1) |
91 | 98 | ||
92 | await setAccessTokensToServers([ server ]) | 99 | await setAccessTokensToServers([ server ]) |
100 | await setDefaultChannelAvatar(server) | ||
101 | await setDefaultAccountAvatar(server) | ||
93 | }) | 102 | }) |
94 | 103 | ||
95 | it('Should list video categories', async function () { | 104 | it('Should list video categories', async function () { |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index d435f3682..0f8227fd3 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -6,13 +6,14 @@ import { basename } from 'path' | |||
6 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | 6 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' |
7 | import { testFileExistsOrNot, testImage } from '@server/tests/shared' | 7 | import { testFileExistsOrNot, testImage } from '@server/tests/shared' |
8 | import { wait } from '@shared/core-utils' | 8 | import { wait } from '@shared/core-utils' |
9 | import { User, VideoChannel } from '@shared/models' | 9 | import { ActorImageType, User, VideoChannel } from '@shared/models' |
10 | import { | 10 | import { |
11 | cleanupTests, | 11 | cleanupTests, |
12 | createMultipleServers, | 12 | createMultipleServers, |
13 | doubleFollow, | 13 | doubleFollow, |
14 | PeerTubeServer, | 14 | PeerTubeServer, |
15 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
16 | setDefaultAccountAvatar, | ||
16 | setDefaultVideoChannel, | 17 | setDefaultVideoChannel, |
17 | waitJobs | 18 | waitJobs |
18 | } from '@shared/server-commands' | 19 | } from '@shared/server-commands' |
@@ -44,6 +45,7 @@ describe('Test video channels', function () { | |||
44 | 45 | ||
45 | await setAccessTokensToServers(servers) | 46 | await setAccessTokensToServers(servers) |
46 | await setDefaultVideoChannel(servers) | 47 | await setDefaultVideoChannel(servers) |
48 | await setDefaultAccountAvatar(servers) | ||
47 | 49 | ||
48 | await doubleFollow(servers[0], servers[1]) | 50 | await doubleFollow(servers[0], servers[1]) |
49 | }) | 51 | }) |
@@ -281,14 +283,19 @@ describe('Test video channels', function () { | |||
281 | 283 | ||
282 | for (const server of servers) { | 284 | for (const server of servers) { |
283 | const videoChannel = await findChannel(server, secondVideoChannelId) | 285 | const videoChannel = await findChannel(server, secondVideoChannelId) |
286 | const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR] | ||
284 | 287 | ||
285 | avatarPaths[server.port] = videoChannel.avatar.path | 288 | expect(videoChannel.avatars.length).to.equal(expectedSizes.length, 'Expected avatars to be generated in all sizes') |
286 | await testImage(server.url, 'avatar-resized', avatarPaths[server.port], '.png') | ||
287 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
288 | 289 | ||
289 | const row = await server.sql.getActorImage(basename(avatarPaths[server.port])) | 290 | for (const avatar of videoChannel.avatars) { |
290 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) | 291 | avatarPaths[server.port] = avatar.path |
291 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) | 292 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png') |
293 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
294 | |||
295 | const row = await server.sql.getActorImage(basename(avatarPaths[server.port])) | ||
296 | |||
297 | expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true) | ||
298 | } | ||
292 | } | 299 | } |
293 | }) | 300 | }) |
294 | 301 | ||
@@ -308,19 +315,18 @@ describe('Test video channels', function () { | |||
308 | for (const server of servers) { | 315 | for (const server of servers) { |
309 | const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host }) | 316 | const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host }) |
310 | 317 | ||
311 | bannerPaths[server.port] = videoChannel.banner.path | 318 | bannerPaths[server.port] = videoChannel.banners[0].path |
312 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) | 319 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) |
313 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) | 320 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) |
314 | 321 | ||
315 | const row = await server.sql.getActorImage(basename(bannerPaths[server.port])) | 322 | const row = await server.sql.getActorImage(basename(bannerPaths[server.port])) |
316 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) | 323 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height) |
317 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) | 324 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width) |
318 | } | 325 | } |
319 | }) | 326 | }) |
320 | 327 | ||
321 | it('Should delete the video channel avatar', async function () { | 328 | it('Should delete the video channel avatar', async function () { |
322 | this.timeout(15000) | 329 | this.timeout(15000) |
323 | |||
324 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' }) | 330 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' }) |
325 | 331 | ||
326 | await waitJobs(servers) | 332 | await waitJobs(servers) |
@@ -329,7 +335,7 @@ describe('Test video channels', function () { | |||
329 | const videoChannel = await findChannel(server, secondVideoChannelId) | 335 | const videoChannel = await findChannel(server, secondVideoChannelId) |
330 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) | 336 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) |
331 | 337 | ||
332 | expect(videoChannel.avatar).to.be.null | 338 | expect(videoChannel.avatars).to.be.empty |
333 | } | 339 | } |
334 | }) | 340 | }) |
335 | 341 | ||
@@ -344,7 +350,7 @@ describe('Test video channels', function () { | |||
344 | const videoChannel = await findChannel(server, secondVideoChannelId) | 350 | const videoChannel = await findChannel(server, secondVideoChannelId) |
345 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) | 351 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) |
346 | 352 | ||
347 | expect(videoChannel.banner).to.be.null | 353 | expect(videoChannel.banners).to.be.empty |
348 | } | 354 | } |
349 | }) | 355 | }) |
350 | 356 | ||
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index 2ae523970..1488ce2b5 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -3,7 +3,15 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { dateIsValid, testImage } from '@server/tests/shared' | 5 | import { dateIsValid, testImage } from '@server/tests/shared' |
6 | import { cleanupTests, CommentsCommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 6 | import { |
7 | cleanupTests, | ||
8 | CommentsCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@shared/server-commands' | ||
7 | 15 | ||
8 | const expect = chai.expect | 16 | const expect = chai.expect |
9 | 17 | ||
@@ -29,7 +37,8 @@ describe('Test video comments', function () { | |||
29 | videoUUID = uuid | 37 | videoUUID = uuid |
30 | videoId = id | 38 | videoId = id |
31 | 39 | ||
32 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) | 40 | await setDefaultChannelAvatar(server) |
41 | await setDefaultAccountAvatar(server) | ||
33 | 42 | ||
34 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | 43 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') |
35 | 44 | ||
@@ -81,7 +90,9 @@ describe('Test video comments', function () { | |||
81 | expect(comment.account.name).to.equal('root') | 90 | expect(comment.account.name).to.equal('root') |
82 | expect(comment.account.host).to.equal('localhost:' + server.port) | 91 | expect(comment.account.host).to.equal('localhost:' + server.port) |
83 | 92 | ||
84 | await testImage(server.url, 'avatar-resized', comment.account.avatar.path, '.png') | 93 | for (const avatar of comment.account.avatars) { |
94 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
95 | } | ||
85 | 96 | ||
86 | expect(comment.totalReplies).to.equal(0) | 97 | expect(comment.totalReplies).to.equal(0) |
87 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) | 98 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) |
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 34327334f..1e8dbef02 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -20,6 +20,7 @@ import { | |||
20 | PeerTubeServer, | 20 | PeerTubeServer, |
21 | PlaylistsCommand, | 21 | PlaylistsCommand, |
22 | setAccessTokensToServers, | 22 | setAccessTokensToServers, |
23 | setDefaultAccountAvatar, | ||
23 | setDefaultVideoChannel, | 24 | setDefaultVideoChannel, |
24 | waitJobs | 25 | waitJobs |
25 | } from '@shared/server-commands' | 26 | } from '@shared/server-commands' |
@@ -79,6 +80,7 @@ describe('Test video playlists', function () { | |||
79 | // Get the access tokens | 80 | // Get the access tokens |
80 | await setAccessTokensToServers(servers) | 81 | await setAccessTokensToServers(servers) |
81 | await setDefaultVideoChannel(servers) | 82 | await setDefaultVideoChannel(servers) |
83 | await setDefaultAccountAvatar(servers) | ||
82 | 84 | ||
83 | // Server 1 and server 2 follow each other | 85 | // Server 1 and server 2 follow each other |
84 | await doubleFollow(servers[0], servers[1]) | 86 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 0254662c5..317de90a9 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { pick } from '@shared/core-utils' | 5 | import { pick } from '@shared/core-utils' |
6 | import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | createMultipleServers, | 9 | createMultipleServers, |
@@ -10,10 +11,10 @@ import { | |||
10 | makeGetRequest, | 11 | makeGetRequest, |
11 | PeerTubeServer, | 12 | PeerTubeServer, |
12 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | 15 | setDefaultVideoChannel, |
14 | waitJobs | 16 | waitJobs |
15 | } from '@shared/server-commands' | 17 | } from '@shared/server-commands' |
16 | import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models' | ||
17 | 18 | ||
18 | describe('Test videos filter', function () { | 19 | describe('Test videos filter', function () { |
19 | let servers: PeerTubeServer[] | 20 | let servers: PeerTubeServer[] |
@@ -29,6 +30,7 @@ describe('Test videos filter', function () { | |||
29 | 30 | ||
30 | await setAccessTokensToServers(servers) | 31 | await setAccessTokensToServers(servers) |
31 | await setDefaultVideoChannel(servers) | 32 | await setDefaultVideoChannel(servers) |
33 | await setDefaultAccountAvatar(servers) | ||
32 | 34 | ||
33 | for (const server of servers) { | 35 | for (const server of servers) { |
34 | const moderator = { username: 'moderator', password: 'my super password' } | 36 | const moderator = { username: 'moderator', password: 'my super password' } |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index a723ed8b4..3ca7c19ea 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -51,7 +51,7 @@ async function assertCountAreOkay (servers: PeerTubeServer[]) { | |||
51 | expect(thumbnailsCount).to.equal(6) | 51 | expect(thumbnailsCount).to.equal(6) |
52 | 52 | ||
53 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
54 | expect(avatarsCount).to.equal(2) | 54 | expect(avatarsCount).to.equal(4) |
55 | 55 | ||
56 | const hlsRootCount = await countFiles(server, 'streaming-playlists/hls') | 56 | const hlsRootCount = await countFiles(server, 'streaming-playlists/hls') |
57 | expect(hlsRootCount).to.equal(2) | 57 | expect(hlsRootCount).to.equal(2) |
@@ -87,23 +87,28 @@ describe('Test prune storage scripts', function () { | |||
87 | 87 | ||
88 | await doubleFollow(servers[0], servers[1]) | 88 | await doubleFollow(servers[0], servers[1]) |
89 | 89 | ||
90 | // Lazy load the remote avatar | 90 | // Lazy load the remote avatars |
91 | { | 91 | { |
92 | const account = await servers[0].accounts.get({ accountName: 'root@localhost:' + servers[1].port }) | 92 | const account = await servers[0].accounts.get({ accountName: 'root@localhost:' + servers[1].port }) |
93 | await makeGetRequest({ | 93 | |
94 | url: servers[0].url, | 94 | for (const avatar of account.avatars) { |
95 | path: account.avatar.path, | 95 | await makeGetRequest({ |
96 | expectedStatus: HttpStatusCode.OK_200 | 96 | url: servers[0].url, |
97 | }) | 97 | path: avatar.path, |
98 | expectedStatus: HttpStatusCode.OK_200 | ||
99 | }) | ||
100 | } | ||
98 | } | 101 | } |
99 | 102 | ||
100 | { | 103 | { |
101 | const account = await servers[1].accounts.get({ accountName: 'root@localhost:' + servers[0].port }) | 104 | const account = await servers[1].accounts.get({ accountName: 'root@localhost:' + servers[0].port }) |
102 | await makeGetRequest({ | 105 | for (const avatar of account.avatars) { |
103 | url: servers[1].url, | 106 | await makeGetRequest({ |
104 | path: account.avatar.path, | 107 | url: servers[1].url, |
105 | expectedStatus: HttpStatusCode.OK_200 | 108 | path: avatar.path, |
106 | }) | 109 | expectedStatus: HttpStatusCode.OK_200 |
110 | }) | ||
111 | } | ||
107 | } | 112 | } |
108 | 113 | ||
109 | await wait(1000) | 114 | await wait(1000) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 4dcd77cca..320dc3333 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { XMLParser, XMLValidator } from 'fast-xml-parser' | 5 | import { XMLParser, XMLValidator } from 'fast-xml-parser' |
6 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | createMultipleServers, | 9 | createMultipleServers, |
@@ -11,9 +12,9 @@ import { | |||
11 | makeGetRequest, | 12 | makeGetRequest, |
12 | PeerTubeServer, | 13 | PeerTubeServer, |
13 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | setDefaultChannelAvatar, | ||
14 | waitJobs | 16 | waitJobs |
15 | } from '@shared/server-commands' | 17 | } from '@shared/server-commands' |
16 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
17 | 18 | ||
18 | chai.use(require('chai-xml')) | 19 | chai.use(require('chai-xml')) |
19 | chai.use(require('chai-json-schema')) | 20 | chai.use(require('chai-json-schema')) |
@@ -44,6 +45,7 @@ describe('Test syndication feeds', () => { | |||
44 | }) | 45 | }) |
45 | 46 | ||
46 | await setAccessTokensToServers([ ...servers, serverHLSOnly ]) | 47 | await setAccessTokensToServers([ ...servers, serverHLSOnly ]) |
48 | await setDefaultChannelAvatar(servers[0]) | ||
47 | await doubleFollow(servers[0], servers[1]) | 49 | await doubleFollow(servers[0], servers[1]) |
48 | 50 | ||
49 | { | 51 | { |
diff --git a/server/tests/fixtures/avatar-resized.gif b/server/tests/fixtures/avatar-resized-120x120.gif index 81a82189e..81a82189e 100644 --- a/server/tests/fixtures/avatar-resized.gif +++ b/server/tests/fixtures/avatar-resized-120x120.gif | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized.png b/server/tests/fixtures/avatar-resized-120x120.png index 9d84151f8..9d84151f8 100644 --- a/server/tests/fixtures/avatar-resized.png +++ b/server/tests/fixtures/avatar-resized-120x120.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized-48x48.gif b/server/tests/fixtures/avatar-resized-48x48.gif new file mode 100644 index 000000000..5900ff12e --- /dev/null +++ b/server/tests/fixtures/avatar-resized-48x48.gif | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized-48x48.png b/server/tests/fixtures/avatar-resized-48x48.png new file mode 100644 index 000000000..9e5f3b490 --- /dev/null +++ b/server/tests/fixtures/avatar-resized-48x48.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar2-resized.png b/server/tests/fixtures/avatar2-resized-120x120.png index 44149facb..44149facb 100644 --- a/server/tests/fixtures/avatar2-resized.png +++ b/server/tests/fixtures/avatar2-resized-120x120.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar2-resized-48x48.png b/server/tests/fixtures/avatar2-resized-48x48.png new file mode 100644 index 000000000..bb3939b1a --- /dev/null +++ b/server/tests/fixtures/avatar2-resized-48x48.png | |||
Binary files differ | |||
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts index cdc21fdc8..78d3787f0 100644 --- a/server/tests/shared/notifications.ts +++ b/server/tests/shared/notifications.ts | |||
@@ -10,7 +10,14 @@ import { | |||
10 | UserNotificationSettingValue, | 10 | UserNotificationSettingValue, |
11 | UserNotificationType | 11 | UserNotificationType |
12 | } from '@shared/models' | 12 | } from '@shared/models' |
13 | import { createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 13 | import { |
14 | createMultipleServers, | ||
15 | doubleFollow, | ||
16 | PeerTubeServer, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultAccountAvatar, | ||
19 | setDefaultChannelAvatar | ||
20 | } from '@shared/server-commands' | ||
14 | import { MockSmtpServer } from './mock-servers' | 21 | import { MockSmtpServer } from './mock-servers' |
15 | 22 | ||
16 | type CheckerBaseParams = { | 23 | type CheckerBaseParams = { |
@@ -646,6 +653,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an | |||
646 | const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) | 653 | const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) |
647 | 654 | ||
648 | await setAccessTokensToServers(servers) | 655 | await setAccessTokensToServers(servers) |
656 | await setDefaultChannelAvatar(servers) | ||
657 | await setDefaultAccountAvatar(servers) | ||
649 | 658 | ||
650 | if (serversCount > 1) { | 659 | if (serversCount > 1) { |
651 | await doubleFollow(servers[0], servers[1]) | 660 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/types/models/actor/actor-image.ts b/server/types/models/actor/actor-image.ts index 521b4cc59..e8f32b71e 100644 --- a/server/types/models/actor/actor-image.ts +++ b/server/types/models/actor/actor-image.ts | |||
@@ -9,4 +9,4 @@ export type MActorImage = ActorImageModel | |||
9 | 9 | ||
10 | export type MActorImageFormattable = | 10 | export type MActorImageFormattable = |
11 | FunctionProperties<MActorImage> & | 11 | FunctionProperties<MActorImage> & |
12 | Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'> | 12 | Pick<MActorImage, 'width' | 'filename' | 'createdAt' | 'updatedAt'> |
diff --git a/server/types/models/actor/actor.ts b/server/types/models/actor/actor.ts index 9ce97094f..280256bab 100644 --- a/server/types/models/actor/actor.ts +++ b/server/types/models/actor/actor.ts | |||
@@ -10,7 +10,7 @@ type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M> | |||
10 | 10 | ||
11 | // ############################################################################ | 11 | // ############################################################################ |
12 | 12 | ||
13 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'> | 13 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'ActorFollowers' | 'Server' | 'Banners'> |
14 | 14 | ||
15 | // ############################################################################ | 15 | // ############################################################################ |
16 | 16 | ||
@@ -35,7 +35,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ | |||
35 | export type MActorDefaultLight = | 35 | export type MActorDefaultLight = |
36 | MActorLight & | 36 | MActorLight & |
37 | Use<'Server', MServerHost> & | 37 | Use<'Server', MServerHost> & |
38 | Use<'Avatar', MActorImage> | 38 | Use<'Avatars', MActorImage[]> |
39 | 39 | ||
40 | export type MActorAccountId = | 40 | export type MActorAccountId = |
41 | MActor & | 41 | MActor & |
@@ -78,13 +78,13 @@ export type MActorServer = | |||
78 | 78 | ||
79 | export type MActorImages = | 79 | export type MActorImages = |
80 | MActor & | 80 | MActor & |
81 | Use<'Avatar', MActorImage> & | 81 | Use<'Avatars', MActorImage[]> & |
82 | UseOpt<'Banner', MActorImage> | 82 | UseOpt<'Banners', MActorImage[]> |
83 | 83 | ||
84 | export type MActorDefault = | 84 | export type MActorDefault = |
85 | MActor & | 85 | MActor & |
86 | Use<'Server', MServer> & | 86 | Use<'Server', MServer> & |
87 | Use<'Avatar', MActorImage> | 87 | Use<'Avatars', MActorImage[]> |
88 | 88 | ||
89 | export type MActorDefaultChannelId = | 89 | export type MActorDefaultChannelId = |
90 | MActorDefault & | 90 | MActorDefault & |
@@ -93,8 +93,8 @@ export type MActorDefaultChannelId = | |||
93 | export type MActorDefaultBanner = | 93 | export type MActorDefaultBanner = |
94 | MActor & | 94 | MActor & |
95 | Use<'Server', MServer> & | 95 | Use<'Server', MServer> & |
96 | Use<'Avatar', MActorImage> & | 96 | Use<'Avatars', MActorImage[]> & |
97 | Use<'Banner', MActorImage> | 97 | Use<'Banners', MActorImage[]> |
98 | 98 | ||
99 | // Actor with channel that is associated to an account and its actor | 99 | // Actor with channel that is associated to an account and its actor |
100 | // Actor -> VideoChannel -> Account -> Actor | 100 | // Actor -> VideoChannel -> Account -> Actor |
@@ -105,8 +105,8 @@ export type MActorChannelAccountActor = | |||
105 | export type MActorFull = | 105 | export type MActorFull = |
106 | MActor & | 106 | MActor & |
107 | Use<'Server', MServer> & | 107 | Use<'Server', MServer> & |
108 | Use<'Avatar', MActorImage> & | 108 | Use<'Avatars', MActorImage[]> & |
109 | Use<'Banner', MActorImage> & | 109 | Use<'Banners', MActorImage[]> & |
110 | Use<'Account', MAccount> & | 110 | Use<'Account', MAccount> & |
111 | Use<'VideoChannel', MChannelAccountActor> | 111 | Use<'VideoChannel', MChannelAccountActor> |
112 | 112 | ||
@@ -114,8 +114,8 @@ export type MActorFull = | |||
114 | export type MActorFullActor = | 114 | export type MActorFullActor = |
115 | MActor & | 115 | MActor & |
116 | Use<'Server', MServer> & | 116 | Use<'Server', MServer> & |
117 | Use<'Avatar', MActorImage> & | 117 | Use<'Avatars', MActorImage[]> & |
118 | Use<'Banner', MActorImage> & | 118 | Use<'Banners', MActorImage[]> & |
119 | Use<'Account', MAccountDefault> & | 119 | Use<'Account', MAccountDefault> & |
120 | Use<'VideoChannel', MChannelAccountDefault> | 120 | Use<'VideoChannel', MChannelAccountDefault> |
121 | 121 | ||
@@ -125,9 +125,9 @@ export type MActorFullActor = | |||
125 | 125 | ||
126 | export type MActorSummary = | 126 | export type MActorSummary = |
127 | FunctionProperties<MActor> & | 127 | FunctionProperties<MActor> & |
128 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & | 128 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId'> & |
129 | Use<'Server', MServerHost> & | 129 | Use<'Server', MServerHost> & |
130 | Use<'Avatar', MActorImage> | 130 | Use<'Avatars', MActorImage[]> |
131 | 131 | ||
132 | export type MActorSummaryBlocks = | 132 | export type MActorSummaryBlocks = |
133 | MActorSummary & | 133 | MActorSummary & |
@@ -145,21 +145,22 @@ export type MActorSummaryFormattable = | |||
145 | FunctionProperties<MActor> & | 145 | FunctionProperties<MActor> & |
146 | Pick<MActor, 'url' | 'preferredUsername'> & | 146 | Pick<MActor, 'url' | 'preferredUsername'> & |
147 | Use<'Server', MServerHost> & | 147 | Use<'Server', MServerHost> & |
148 | Use<'Avatar', MActorImageFormattable> | 148 | Use<'Avatars', MActorImageFormattable[]> |
149 | 149 | ||
150 | export type MActorFormattable = | 150 | export type MActorFormattable = |
151 | MActorSummaryFormattable & | 151 | MActorSummaryFormattable & |
152 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'remoteCreatedAt' | 'bannerId' | 'avatarId'> & | 152 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'remoteCreatedAt'> & |
153 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & | 153 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & |
154 | UseOpt<'Banner', MActorImageFormattable> | 154 | UseOpt<'Banners', MActorImageFormattable[]> & |
155 | UseOpt<'Avatars', MActorImageFormattable[]> | ||
155 | 156 | ||
156 | type MActorAPBase = | 157 | type MActorAPBase = |
157 | MActor & | 158 | MActor & |
158 | Use<'Avatar', MActorImage> | 159 | Use<'Avatars', MActorImage[]> |
159 | 160 | ||
160 | export type MActorAPAccount = | 161 | export type MActorAPAccount = |
161 | MActorAPBase | 162 | MActorAPBase |
162 | 163 | ||
163 | export type MActorAPChannel = | 164 | export type MActorAPChannel = |
164 | MActorAPBase & | 165 | MActorAPBase & |
165 | Use<'Banner', MActorImage> | 166 | Use<'Banners', MActorImage[]> |
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index db9ec0400..d4715a0b6 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -21,6 +21,7 @@ type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationMo | |||
21 | // ############################################################################ | 21 | // ############################################################################ |
22 | 22 | ||
23 | export module UserNotificationIncludes { | 23 | export module UserNotificationIncludes { |
24 | export type ActorImageInclude = Pick<ActorImageModel, 'createdAt' | 'filename' | 'getStaticPath' | 'width' | 'updatedAt'> | ||
24 | 25 | ||
25 | export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'> | 26 | export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'> |
26 | export type VideoIncludeChannel = | 27 | export type VideoIncludeChannel = |
@@ -29,7 +30,7 @@ export module UserNotificationIncludes { | |||
29 | 30 | ||
30 | export type ActorInclude = | 31 | export type ActorInclude = |
31 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 32 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
32 | PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> & | 33 | PickWith<ActorModel, 'Avatars', ActorImageInclude[]> & |
33 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | 34 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> |
34 | 35 | ||
35 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> | 36 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> |
@@ -75,7 +76,7 @@ export module UserNotificationIncludes { | |||
75 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 76 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
76 | PickWith<ActorModel, 'Account', AccountInclude> & | 77 | PickWith<ActorModel, 'Account', AccountInclude> & |
77 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & | 78 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & |
78 | PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> | 79 | PickWithOpt<ActorModel, 'Avatars', ActorImageInclude[]> |
79 | 80 | ||
80 | export type ActorFollowing = | 81 | export type ActorFollowing = |
81 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & | 82 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & |
@@ -98,7 +99,7 @@ export module UserNotificationIncludes { | |||
98 | // ############################################################################ | 99 | // ############################################################################ |
99 | 100 | ||
100 | export type MUserNotification = | 101 | export type MUserNotification = |
101 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | | 102 | Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' | |
102 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> | 103 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> |
103 | 104 | ||
104 | // ############################################################################ | 105 | // ############################################################################ |
@@ -106,7 +107,7 @@ export type MUserNotification = | |||
106 | export type UserNotificationModelForApi = | 107 | export type UserNotificationModelForApi = |
107 | MUserNotification & | 108 | MUserNotification & |
108 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & | 109 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & |
109 | Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & | 110 | Use<'VideoComment', UserNotificationIncludes.VideoCommentInclude> & |
110 | Use<'Abuse', UserNotificationIncludes.AbuseInclude> & | 111 | Use<'Abuse', UserNotificationIncludes.AbuseInclude> & |
111 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & | 112 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & |
112 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & | 113 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & |