From 7d9ba5c08999c6482f0bc5e0c09c6f55b7724090 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 11:15:29 +0200 Subject: Cleanup models directory organization --- server/controllers/activitypub/client.ts | 2 +- server/controllers/api/server/follows.ts | 18 +- server/controllers/api/server/server-blocklist.ts | 2 +- server/controllers/api/users/index.ts | 2 +- server/controllers/api/users/me.ts | 2 +- server/controllers/api/users/my-blocklist.ts | 2 +- server/controllers/api/users/my-history.ts | 2 +- server/controllers/api/users/my-notifications.ts | 14 +- server/controllers/api/users/my-subscriptions.ts | 2 +- server/controllers/api/videos/watching.ts | 2 +- server/controllers/lazy-static.ts | 2 +- server/controllers/static.ts | 2 +- server/helpers/actor.ts | 2 +- server/helpers/middlewares/accounts.ts | 2 +- server/helpers/signup.ts | 2 +- server/helpers/webfinger.ts | 6 +- server/initializers/checker-after-init.ts | 2 +- server/initializers/database.ts | 14 +- server/initializers/installer.ts | 2 +- server/lib/activitypub/actor.ts | 4 +- server/lib/activitypub/audience.ts | 2 +- server/lib/activitypub/process/process-accept.ts | 4 +- server/lib/activitypub/process/process-delete.ts | 2 +- server/lib/activitypub/process/process-follow.ts | 14 +- server/lib/activitypub/process/process-reject.ts | 2 +- server/lib/activitypub/process/process-undo.ts | 4 +- server/lib/activitypub/process/process-update.ts | 20 +- server/lib/activitypub/send/send-delete.ts | 2 +- server/lib/activitypub/send/send-view.ts | 2 +- server/lib/activitypub/send/utils.ts | 12 +- server/lib/auth/oauth-model.ts | 4 +- .../lib/job-queue/handlers/activitypub-follow.ts | 20 +- .../job-queue/handlers/activitypub-refresher.ts | 8 +- server/lib/job-queue/handlers/actor-keys.ts | 2 +- .../handlers/utils/activitypub-http-utils.ts | 8 +- server/lib/job-queue/handlers/video-file-import.ts | 2 +- server/lib/job-queue/handlers/video-transcoding.ts | 2 +- server/lib/live-manager.ts | 2 +- server/lib/moderation.ts | 4 +- server/lib/notifier.ts | 4 +- server/lib/plugins/plugin-helpers-builder.ts | 2 +- server/lib/redundancy.ts | 12 +- server/lib/schedulers/actor-follow-scheduler.ts | 4 +- .../lib/schedulers/auto-follow-index-instances.ts | 2 +- .../lib/schedulers/remove-old-history-scheduler.ts | 2 +- server/lib/stat-manager.ts | 4 +- server/lib/user.ts | 9 +- server/middlewares/validators/follows.ts | 16 +- .../middlewares/validators/user-subscriptions.ts | 8 +- server/middlewares/validators/users.ts | 4 +- .../validators/videos/video-channels.ts | 4 +- server/middlewares/validators/webfinger.ts | 6 +- server/models/account/account-blocklist.ts | 2 +- server/models/account/account-video-rate.ts | 2 +- server/models/account/account.ts | 8 +- server/models/account/actor-image.ts | 100 --- server/models/account/user-notification-setting.ts | 221 ----- server/models/account/user-notification.ts | 665 -------------- server/models/account/user-video-history.ts | 100 --- server/models/account/user.ts | 967 --------------------- server/models/activitypub/actor-follow.ts | 721 --------------- server/models/activitypub/actor.ts | 699 --------------- server/models/actor/actor-follow.ts | 721 +++++++++++++++ server/models/actor/actor-image.ts | 100 +++ server/models/actor/actor.ts | 699 +++++++++++++++ server/models/oauth/oauth-token.ts | 4 +- server/models/redundancy/video-redundancy.ts | 2 +- server/models/server/server.ts | 2 +- server/models/user/user-notification-setting.ts | 221 +++++ server/models/user/user-notification.ts | 665 ++++++++++++++ server/models/user/user-video-history.ts | 100 +++ server/models/user/user.ts | 967 +++++++++++++++++++++ server/models/video/video-channel.ts | 6 +- server/models/video/video-comment.ts | 2 +- server/models/video/video-import.ts | 4 +- server/models/video/video-playlist.ts | 2 +- server/models/video/video-share.ts | 2 +- server/models/video/video.ts | 8 +- server/types/models/abuse/abuse-message.ts | 20 + server/types/models/abuse/abuse.ts | 114 +++ server/types/models/abuse/index.ts | 2 + server/types/models/account/account.ts | 6 +- server/types/models/account/actor-follow.ts | 65 -- server/types/models/account/actor-image.ts | 12 - server/types/models/account/actor.ts | 166 ---- server/types/models/account/index.ts | 3 - server/types/models/actor/actor-follow.ts | 65 ++ server/types/models/actor/actor-image.ts | 12 + server/types/models/actor/actor.ts | 165 ++++ server/types/models/actor/index.ts | 3 + server/types/models/index.ts | 3 +- server/types/models/moderation/abuse-message.ts | 20 - server/types/models/moderation/abuse.ts | 114 --- server/types/models/moderation/index.ts | 2 - .../types/models/user/user-notification-setting.ts | 2 +- server/types/models/user/user-notification.ts | 8 +- server/types/models/user/user-video-history.ts | 2 +- server/types/models/user/user.ts | 2 +- server/types/models/video/video-channels.ts | 6 +- server/types/models/video/video-share.ts | 4 +- .../types/plugins/register-server-option.model.ts | 4 +- 101 files changed, 4036 insertions(+), 4033 deletions(-) delete mode 100644 server/models/account/actor-image.ts delete mode 100644 server/models/account/user-notification-setting.ts delete mode 100644 server/models/account/user-notification.ts delete mode 100644 server/models/account/user-video-history.ts delete mode 100644 server/models/account/user.ts delete mode 100644 server/models/activitypub/actor-follow.ts delete mode 100644 server/models/activitypub/actor.ts create mode 100644 server/models/actor/actor-follow.ts create mode 100644 server/models/actor/actor-image.ts create mode 100644 server/models/actor/actor.ts create mode 100644 server/models/user/user-notification-setting.ts create mode 100644 server/models/user/user-notification.ts create mode 100644 server/models/user/user-video-history.ts create mode 100644 server/models/user/user.ts create mode 100644 server/types/models/abuse/abuse-message.ts create mode 100644 server/types/models/abuse/abuse.ts create mode 100644 server/types/models/abuse/index.ts delete mode 100644 server/types/models/account/actor-follow.ts delete mode 100644 server/types/models/account/actor-image.ts delete mode 100644 server/types/models/account/actor.ts create mode 100644 server/types/models/actor/actor-follow.ts create mode 100644 server/types/models/actor/actor-image.ts create mode 100644 server/types/models/actor/actor.ts create mode 100644 server/types/models/actor/index.ts delete mode 100644 server/types/models/moderation/abuse-message.ts delete mode 100644 server/types/models/moderation/abuse.ts delete mode 100644 server/types/models/moderation/index.ts (limited to 'server') diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 1b4acc234..1982e171d 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -30,7 +30,7 @@ import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' import { AccountModel } from '../../models/account/account' import { AccountVideoRateModel } from '../../models/account/account-video-rate' -import { ActorFollowModel } from '../../models/activitypub/actor-follow' +import { ActorFollowModel } from '../../models/actor/actor-follow' import { VideoModel } from '../../models/video/video' import { VideoCaptionModel } from '../../models/video/video-caption' import { VideoCommentModel } from '../../models/video/video-comment' diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 80025bc5b..daeef22de 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts @@ -1,9 +1,15 @@ import * as express from 'express' +import { getServerActor } from '@server/models/application/application' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { UserRight } from '../../../../shared/models/users' import { logger } from '../../../helpers/logger' import { getFormattedObjects } from '../../../helpers/utils' import { SERVER_ACTOR_NAME } from '../../../initializers/constants' +import { sequelizeTypescript } from '../../../initializers/database' +import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' +import { JobQueue } from '../../../lib/job-queue' +import { removeRedundanciesOfServer } from '../../../lib/redundancy' import { asyncMiddleware, authenticate, @@ -19,16 +25,10 @@ import { followingSortValidator, followValidator, getFollowerValidator, - removeFollowingValidator, - listFollowsValidator + listFollowsValidator, + removeFollowingValidator } from '../../../middlewares/validators' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' -import { JobQueue } from '../../../lib/job-queue' -import { removeRedundanciesOfServer } from '../../../lib/redundancy' -import { sequelizeTypescript } from '../../../initializers/database' -import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' -import { getServerActor } from '@server/models/application/application' -import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' +import { ActorFollowModel } from '../../../models/actor/actor-follow' const serverFollowsRouter = express.Router() serverFollowsRouter.get('/following', diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts index 6e341c0fb..a86bc7d19 100644 --- a/server/controllers/api/server/server-blocklist.ts +++ b/server/controllers/api/server/server-blocklist.ts @@ -1,7 +1,7 @@ import 'multer' import * as express from 'express' import { logger } from '@server/helpers/logger' -import { UserNotificationModel } from '@server/models/account/user-notification' +import { UserNotificationModel } from '@server/models/user/user-notification' import { getServerActor } from '@server/models/application/application' import { UserRight } from '../../../../shared/models/users' import { getFormattedObjects } from '../../../helpers/utils' diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index e2b1ea7cd..c655d1648 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -45,7 +45,7 @@ import { usersResetPasswordValidator, usersVerifyEmailValidator } from '../../../middlewares/validators' -import { UserModel } from '../../../models/account/user' +import { UserModel } from '../../../models/user/user' import { meRouter } from './me' import { myAbusesRouter } from './my-abuses' import { myBlocklistRouter } from './my-blocklist' diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 0763d1900..d97652840 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -28,7 +28,7 @@ import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } fro import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' import { AccountModel } from '../../../models/account/account' import { AccountVideoRateModel } from '../../../models/account/account-video-rate' -import { UserModel } from '../../../models/account/user' +import { UserModel } from '../../../models/user/user' import { VideoModel } from '../../../models/video/video' import { VideoImportModel } from '../../../models/video/video-import' diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts index faaef3ac0..a1561b751 100644 --- a/server/controllers/api/users/my-blocklist.ts +++ b/server/controllers/api/users/my-blocklist.ts @@ -20,7 +20,7 @@ import { import { AccountBlocklistModel } from '../../../models/account/account-blocklist' import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' import { ServerBlocklistModel } from '../../../models/server/server-blocklist' -import { UserNotificationModel } from '@server/models/account/user-notification' +import { UserNotificationModel } from '@server/models/user/user-notification' import { logger } from '@server/helpers/logger' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 72c7da373..cff1697ab 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts @@ -9,7 +9,7 @@ import { userHistoryRemoveValidator } from '../../../middlewares' import { getFormattedObjects } from '../../../helpers/utils' -import { UserVideoHistoryModel } from '../../../models/account/user-video-history' +import { UserVideoHistoryModel } from '../../../models/user/user-video-history' import { sequelizeTypescript } from '../../../initializers/database' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 0a9101a46..2909770da 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts @@ -1,5 +1,9 @@ -import * as express from 'express' import 'multer' +import * as express from 'express' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' +import { UserNotificationSetting } from '../../../../shared/models/users' +import { getFormattedObjects } from '../../../helpers/utils' import { asyncMiddleware, asyncRetryTransactionMiddleware, @@ -9,17 +13,13 @@ import { setDefaultSort, userNotificationsSortValidator } from '../../../middlewares' -import { getFormattedObjects } from '../../../helpers/utils' -import { UserNotificationModel } from '../../../models/account/user-notification' -import { meRouter } from './me' import { listUserNotificationsValidator, markAsReadUserNotificationsValidator, updateNotificationSettingsValidator } from '../../../middlewares/validators/user-notifications' -import { UserNotificationSetting } from '../../../../shared/models/users' -import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' -import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' +import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' +import { meRouter } from './me' const myNotificationsRouter = express.Router() diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index 56b93276f..46a73d49e 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts @@ -27,7 +27,7 @@ import { userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { VideoModel } from '../../../models/video/video' const mySubscriptionsRouter = express.Router() diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts index 627f12aa9..08190e583 100644 --- a/server/controllers/api/videos/watching.ts +++ b/server/controllers/api/videos/watching.ts @@ -1,7 +1,7 @@ import * as express from 'express' import { UserWatchingVideo } from '../../../../shared' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' -import { UserVideoHistoryModel } from '../../../models/account/user-video-history' +import { UserVideoHistoryModel } from '../../../models/user/user-video-history' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' const watchingRouter = express.Router() diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 6f71fdb16..25d3b49b4 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts @@ -7,7 +7,7 @@ import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image' import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' import { asyncMiddleware } from '../middlewares' -import { ActorImageModel } from '../models/account/actor-image' +import { ActorImageModel } from '../models/actor/actor-image' const lazyStaticRouter = express.Router() diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 5cf4e9575..97c48b1d7 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts @@ -20,7 +20,7 @@ import { import { getThemeOrDefault } from '../lib/plugins/theme-utils' import { asyncMiddleware } from '../middlewares' import { cacheRoute } from '../middlewares/cache' -import { UserModel } from '../models/account/user' +import { UserModel } from '../models/user/user' import { VideoModel } from '../models/video/video' import { VideoCommentModel } from '../models/video/video-comment' diff --git a/server/helpers/actor.ts b/server/helpers/actor.ts index a60d3ed5d..5f742505b 100644 --- a/server/helpers/actor.ts +++ b/server/helpers/actor.ts @@ -1,5 +1,5 @@ -import { ActorModel } from '../models/activitypub/actor' +import { ActorModel } from '../models/actor/actor' import { MActorAccountChannelId, MActorFull } from '../types/models' type ActorFetchByUrlType = 'all' | 'association-ids' diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index 13ae6cdf4..5addd3e1a 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts @@ -1,5 +1,5 @@ import { Response } from 'express' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { AccountModel } from '../../models/account/account' import { MAccountDefault } from '../../types/models' diff --git a/server/helpers/signup.ts b/server/helpers/signup.ts index ed872539b..8fa81e601 100644 --- a/server/helpers/signup.ts +++ b/server/helpers/signup.ts @@ -1,4 +1,4 @@ -import { UserModel } from '../models/account/user' +import { UserModel } from '../models/user/user' import * as ipaddr from 'ipaddr.js' import { CONFIG } from '../initializers/config' diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts index da7e88077..33367f651 100644 --- a/server/helpers/webfinger.ts +++ b/server/helpers/webfinger.ts @@ -1,10 +1,10 @@ import * as WebFinger from 'webfinger.js' import { WebFingerData } from '../../shared' -import { ActorModel } from '../models/activitypub/actor' -import { isTestInstance } from './core-utils' -import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' import { WEBSERVER } from '../initializers/constants' +import { ActorModel } from '../models/actor/actor' import { MActorFull } from '../types/models' +import { isTestInstance } from './core-utils' +import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' const webfinger = new WebFinger({ webfist_fallback: false, diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index a93c8b7fd..911734fa0 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts @@ -7,7 +7,7 @@ import { RecentlyAddedStrategy } from '../../shared/models/redundancy' import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils' import { isArray } from '../helpers/custom-validators/misc' import { logger } from '../helpers/logger' -import { UserModel } from '../models/account/user' +import { UserModel } from '../models/user/user' import { ApplicationModel, getServerActor } from '../models/application/application' import { OAuthClientModel } from '../models/oauth/oauth-client' import { CONFIG, isEmailEnabled } from './config' diff --git a/server/initializers/database.ts b/server/initializers/database.ts index edf12bc41..75a13ec8b 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -2,6 +2,9 @@ import { QueryTypes, Transaction } from 'sequelize' import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' import { TrackerModel } from '@server/models/server/tracker' import { VideoTrackerModel } from '@server/models/server/video-tracker' +import { UserModel } from '@server/models/user/user' +import { UserNotificationModel } from '@server/models/user/user-notification' +import { UserVideoHistoryModel } from '@server/models/user/user-video-history' import { isTestInstance } from '../helpers/core-utils' import { logger } from '../helpers/logger' import { AbuseModel } from '../models/abuse/abuse' @@ -11,13 +14,9 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse' import { AccountModel } from '../models/account/account' import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountVideoRateModel } from '../models/account/account-video-rate' -import { ActorImageModel } from '../models/account/actor-image' -import { UserModel } from '../models/account/user' -import { UserNotificationModel } from '../models/account/user-notification' -import { UserNotificationSettingModel } from '../models/account/user-notification-setting' -import { UserVideoHistoryModel } from '../models/account/user-video-history' -import { ActorModel } from '../models/activitypub/actor' -import { ActorFollowModel } from '../models/activitypub/actor-follow' +import { ActorModel } from '../models/actor/actor' +import { ActorFollowModel } from '../models/actor/actor-follow' +import { ActorImageModel } from '../models/actor/actor-image' import { ApplicationModel } from '../models/application/application' import { OAuthClientModel } from '../models/oauth/oauth-client' import { OAuthTokenModel } from '../models/oauth/oauth-token' @@ -25,6 +24,7 @@ import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' import { PluginModel } from '../models/server/plugin' import { ServerModel } from '../models/server/server' import { ServerBlocklistModel } from '../models/server/server-blocklist' +import { UserNotificationSettingModel } from '../models/user/user-notification-setting' import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' import { TagModel } from '../models/video/tag' import { ThumbnailModel } from '../models/video/thumbnail' diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 8dcff64e2..676f88653 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -2,7 +2,7 @@ import * as passwordGenerator from 'password-generator' import { UserRole } from '../../shared' import { logger } from '../helpers/logger' import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user' -import { UserModel } from '../models/account/user' +import { UserModel } from '../models/user/user' import { ApplicationModel } from '../models/application/application' import { OAuthClientModel } from '../models/oauth/oauth-client' import { applicationExist, clientsExist, usersExist } from './checker-after-init' diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 5fe7381c9..8865b6277 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -20,8 +20,8 @@ import { getUrlFromWebfinger } from '../../helpers/webfinger' import { MIMETYPES, WEBSERVER } from '../../initializers/constants' import { sequelizeTypescript } from '../../initializers/database' import { AccountModel } from '../../models/account/account' -import { ActorImageModel } from '../../models/account/actor-image' -import { ActorModel } from '../../models/activitypub/actor' +import { ActorModel } from '../../models/actor/actor' +import { ActorImageModel } from '../../models/actor/actor-image' import { ServerModel } from '../../models/server/server' import { VideoChannelModel } from '../../models/video/video-channel' import { diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index 2986714d3..d0558f191 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts @@ -1,7 +1,7 @@ import { Transaction } from 'sequelize' import { ActivityAudience } from '../../../shared/models/activitypub' import { ACTIVITY_PUB } from '../../initializers/constants' -import { ActorModel } from '../../models/activitypub/actor' +import { ActorModel } from '../../models/actor/actor' import { VideoModel } from '../../models/video/video' import { VideoShareModel } from '../../models/video/video-share' import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models' diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 1799829f8..8ad470cf4 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts @@ -1,8 +1,8 @@ import { ActivityAccept } from '../../../../shared/models/activitypub' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' -import { addFetchOutboxJob } from '../actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorDefault, MActorSignature } from '../../../types/models' +import { addFetchOutboxJob } from '../actor' async function processAcceptActivity (options: APProcessorOptions) { const { byActor: targetActor, inboxActor } = options diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index 88a968318..20214246c 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts @@ -2,7 +2,7 @@ import { ActivityDelete } from '../../../../shared/models/activitypub' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' -import { ActorModel } from '../../../models/activitypub/actor' +import { ActorModel } from '../../../models/actor/actor' import { VideoModel } from '../../../models/video/video' import { VideoCommentModel } from '../../../models/video/video-comment' import { VideoPlaylistModel } from '../../../models/video/video-playlist' diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 38d684512..9009c6469 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -1,17 +1,17 @@ +import { getServerActor } from '@server/models/application/application' import { ActivityFollow } from '../../../../shared/models/activitypub' +import { getAPId } from '../../../helpers/activitypub' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' -import { sequelizeTypescript } from '../../../initializers/database' -import { ActorModel } from '../../../models/activitypub/actor' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' -import { sendAccept, sendReject } from '../send' -import { Notifier } from '../../notifier' -import { getAPId } from '../../../helpers/activitypub' import { CONFIG } from '../../../initializers/config' +import { sequelizeTypescript } from '../../../initializers/database' +import { ActorModel } from '../../../models/actor/actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorFollowActors, MActorSignature } from '../../../types/models' +import { Notifier } from '../../notifier' import { autoFollowBackIfNeeded } from '../follow' -import { getServerActor } from '@server/models/application/application' +import { sendAccept, sendReject } from '../send' async function processFollowActivity (options: APProcessorOptions) { const { activity, byActor } = options diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts index 03b669fd9..7f7ab305f 100644 --- a/server/lib/activitypub/process/process-reject.ts +++ b/server/lib/activitypub/process/process-reject.ts @@ -1,6 +1,6 @@ import { ActivityReject } from '../../../../shared/models/activitypub/activity' import { sequelizeTypescript } from '../../../initializers/database' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActor } from '../../../types/models' diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index e520c2f0d..9f031b528 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -4,8 +4,8 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' import { AccountVideoRateModel } from '../../../models/account/account-video-rate' -import { ActorModel } from '../../../models/activitypub/actor' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' +import { ActorModel } from '../../../models/actor/actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' import { VideoShareModel } from '../../../models/video/video-share' import { APProcessorOptions } from '../../../types/activitypub-processor.model' diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 6df9b93b2..6cd9d0fba 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts @@ -1,23 +1,23 @@ +import { isRedundancyAccepted } from '@server/lib/redundancy' +import { ActorImageType } from '@shared/models' import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' +import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' +import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' +import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' import { AccountModel } from '../../../models/account/account' -import { ActorModel } from '../../../models/activitypub/actor' +import { ActorModel } from '../../../models/actor/actor' import { VideoChannelModel } from '../../../models/video/video-channel' +import { APProcessorOptions } from '../../../types/activitypub-processor.model' +import { MAccountIdActor, MActorSignature } from '../../../types/models' import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' -import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' -import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' -import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' import { createOrUpdateCacheFile } from '../cache-file' -import { forwardVideoRelatedActivity } from '../send/utils' -import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' import { createOrUpdateVideoPlaylist } from '../playlist' -import { APProcessorOptions } from '../../../types/activitypub-processor.model' -import { MActorSignature, MAccountIdActor } from '../../../types/models' -import { isRedundancyAccepted } from '@server/lib/redundancy' -import { ActorImageType } from '@shared/models' +import { forwardVideoRelatedActivity } from '../send/utils' +import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' async function processUpdateActivity (options: APProcessorOptions) { const { activity, byActor } = options diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index e0acced18..d31f8c10b 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts @@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' import { getServerActor } from '@server/models/application/application' import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' import { logger } from '../../../helpers/logger' -import { ActorModel } from '../../../models/activitypub/actor' +import { ActorModel } from '../../../models/actor/actor' import { VideoCommentModel } from '../../../models/video/video-comment' import { VideoShareModel } from '../../../models/video/video-share' import { MActorUrl } from '../../../types/models' diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts index 9254dc7c5..153e94295 100644 --- a/server/lib/activitypub/send/send-view.ts +++ b/server/lib/activitypub/send/send-view.ts @@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' import { logger } from '../../../helpers/logger' -import { ActorModel } from '../../../models/activitypub/actor' +import { ActorModel } from '../../../models/actor/actor' import { audiencify, getAudience } from '../audience' import { getLocalVideoViewActivityPubUrl } from '../url' import { sendVideoRelatedActivity } from './utils' diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts index 85a9f009d..db0e91b71 100644 --- a/server/lib/activitypub/send/utils.ts +++ b/server/lib/activitypub/send/utils.ts @@ -1,14 +1,14 @@ import { Transaction } from 'sequelize' +import { getServerActor } from '@server/models/application/application' +import { ContextType } from '@shared/models/activitypub/context' import { Activity, ActivityAudience } from '../../../../shared/models/activitypub' +import { afterCommitIfTransaction } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' -import { ActorModel } from '../../../models/activitypub/actor' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' +import { ActorModel } from '../../../models/actor/actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' +import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' import { JobQueue } from '../../job-queue' import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' -import { afterCommitIfTransaction } from '../../../helpers/database-utils' -import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' -import { getServerActor } from '@server/models/application/application' -import { ContextType } from '@shared/models/activitypub/context' async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { byActor: MActorLight diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index b9c69eb2d..ae728d080 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts @@ -1,7 +1,7 @@ import * as express from 'express' import { AccessDeniedError } from 'oauth2-server' import { PluginManager } from '@server/lib/plugins/plugin-manager' -import { ActorModel } from '@server/models/activitypub/actor' +import { ActorModel } from '@server/models/actor/actor' import { MOAuthClient } from '@server/types/models' import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' import { MUser } from '@server/types/models/user/user' @@ -9,7 +9,7 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model' import { UserRole } from '@shared/models/users/user-role' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' -import { UserModel } from '../../models/account/user' +import { UserModel } from '../../models/user/user' import { OAuthClientModel } from '../../models/oauth/oauth-client' import { OAuthTokenModel } from '../../models/oauth/oauth-token' import { createUserAccountAndChannelAndPlaylist } from '../user' diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 82c95be80..ec8df8969 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts @@ -1,18 +1,18 @@ import * as Bull from 'bull' -import { logger } from '../../../helpers/logger' -import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants' -import { sendFollow } from '../../activitypub/send' +import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url' +import { ActivitypubFollowPayload } from '@shared/models' import { sanitizeHost } from '../../../helpers/core-utils' -import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' -import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' import { retryTransactionWrapper } from '../../../helpers/database-utils' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' -import { ActorModel } from '../../../models/activitypub/actor' -import { Notifier } from '../../notifier' +import { logger } from '../../../helpers/logger' +import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' +import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants' import { sequelizeTypescript } from '../../../initializers/database' +import { ActorModel } from '../../../models/actor/actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' import { MActor, MActorFollowActors, MActorFull } from '../../../types/models' -import { ActivitypubFollowPayload } from '@shared/models' -import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url' +import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' +import { sendFollow } from '../../activitypub/send' +import { Notifier } from '../../notifier' async function processActivityPubFollow (job: Bull.Job) { const payload = job.data as ActivitypubFollowPayload diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 666e56868..c09b1bcc8 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts @@ -1,12 +1,12 @@ import * as Bull from 'bull' +import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist' +import { RefreshPayload } from '@shared/models' import { logger } from '../../../helpers/logger' import { fetchVideoByUrl } from '../../../helpers/video' +import { ActorModel } from '../../../models/actor/actor' +import { VideoPlaylistModel } from '../../../models/video/video-playlist' import { refreshActorIfNeeded } from '../../activitypub/actor' import { refreshVideoIfNeeded } from '../../activitypub/videos' -import { ActorModel } from '../../../models/activitypub/actor' -import { VideoPlaylistModel } from '../../../models/video/video-playlist' -import { RefreshPayload } from '@shared/models' -import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist' async function refreshAPObject (job: Bull.Job) { const payload = job.data as RefreshPayload diff --git a/server/lib/job-queue/handlers/actor-keys.ts b/server/lib/job-queue/handlers/actor-keys.ts index 125307843..3eef565d0 100644 --- a/server/lib/job-queue/handlers/actor-keys.ts +++ b/server/lib/job-queue/handlers/actor-keys.ts @@ -1,6 +1,6 @@ import * as Bull from 'bull' import { generateAndSaveActorKeys } from '@server/lib/activitypub/actor' -import { ActorModel } from '@server/models/activitypub/actor' +import { ActorModel } from '@server/models/actor/actor' import { ActorKeysPayload } from '@shared/models' import { logger } from '../../../helpers/logger' diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index e8a91450d..37e7c1fad 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts @@ -1,10 +1,10 @@ +import { buildDigest } from '@server/helpers/peertube-crypto' +import { getServerActor } from '@server/models/application/application' +import { ContextType } from '@shared/models/activitypub/context' import { buildSignedActivity } from '../../../../helpers/activitypub' -import { ActorModel } from '../../../../models/activitypub/actor' import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' +import { ActorModel } from '../../../../models/actor/actor' import { MActor } from '../../../../types/models' -import { getServerActor } from '@server/models/application/application' -import { buildDigest } from '@server/helpers/peertube-crypto' -import { ContextType } from '@shared/models/activitypub/context' type Payload = { body: T, contextType?: ContextType, signatureActorId?: number } diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 71f2cafcd..8297a1571 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts @@ -3,7 +3,7 @@ import { copy, stat } from 'fs-extra' import { extname } from 'path' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' import { MVideoFullLight } from '@server/types/models' import { VideoFileImportPayload } from '@shared/models' import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index e31bc82f5..8d659daa6 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts @@ -2,7 +2,7 @@ import * as Bull from 'bull' import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video' import { getVideoFilePath } from '@server/lib/video-paths' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' import { HLSTranscodingPayload, diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index fe2a43f61..8e7fd5511 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts @@ -11,7 +11,7 @@ import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution import { logger } from '@server/helpers/logger' import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' import { VideoModel } from '@server/models/video/video' import { VideoFileModel } from '@server/models/video/video-file' import { VideoLiveModel } from '@server/models/video/video-live' diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 925d64902..d83086005 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts @@ -24,8 +24,8 @@ import { VideoObject } from '../../shared/models/activitypub/objects' import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' -import { UserModel } from '../models/account/user' -import { ActorModel } from '../models/activitypub/actor' +import { UserModel } from '../models/user/user' +import { ActorModel } from '../models/actor/actor' import { VideoModel } from '../models/video/video' import { VideoCommentModel } from '../models/video/video-comment' import { sendAbuse } from './activitypub/send/send-flag' diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index da7f7cc05..1f9ff16df 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts @@ -17,8 +17,8 @@ import { VideoPrivacy, VideoState } from '../../shared/models/videos' import { logger } from '../helpers/logger' import { CONFIG } from '../initializers/config' import { AccountBlocklistModel } from '../models/account/account-blocklist' -import { UserModel } from '../models/account/user' -import { UserNotificationModel } from '../models/account/user-notification' +import { UserModel } from '../models/user/user' +import { UserNotificationModel } from '../models/user/user-notification' import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' import { isBlockedByServerOrAccount } from './blocklist' diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index f1bc24d8b..cb1cd4d9a 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts @@ -17,7 +17,7 @@ import { VideoBlacklistCreate } from '@shared/models' import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' import { getServerConfig } from '../config' import { blacklistVideo, unblacklistVideo } from '../video-blacklist' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { const logger = buildPluginLogger(npmName) diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts index da620b607..2a9241249 100644 --- a/server/lib/redundancy.ts +++ b/server/lib/redundancy.ts @@ -1,12 +1,12 @@ -import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' -import { sendUndoCacheFile } from './activitypub/send' import { Transaction } from 'sequelize' -import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models' -import { CONFIG } from '@server/initializers/config' import { logger } from '@server/helpers/logger' -import { ActorFollowModel } from '@server/models/activitypub/actor-follow' -import { Activity } from '@shared/models' +import { CONFIG } from '@server/initializers/config' +import { ActorFollowModel } from '@server/models/actor/actor-follow' import { getServerActor } from '@server/models/application/application' +import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models' +import { Activity } from '@shared/models' +import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' +import { sendUndoCacheFile } from './activitypub/send' async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { const serverActor = await getServerActor() diff --git a/server/lib/schedulers/actor-follow-scheduler.ts b/server/lib/schedulers/actor-follow-scheduler.ts index 598c0211f..1b80316e9 100644 --- a/server/lib/schedulers/actor-follow-scheduler.ts +++ b/server/lib/schedulers/actor-follow-scheduler.ts @@ -1,9 +1,9 @@ import { isTestInstance } from '../../helpers/core-utils' import { logger } from '../../helpers/logger' -import { ActorFollowModel } from '../../models/activitypub/actor-follow' -import { AbstractScheduler } from './abstract-scheduler' import { ACTOR_FOLLOW_SCORE, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' +import { ActorFollowModel } from '../../models/actor/actor-follow' import { ActorFollowScoreCache } from '../files-cache' +import { AbstractScheduler } from './abstract-scheduler' export class ActorFollowScheduler extends AbstractScheduler { diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index 0b8cd1389..aaa5feed5 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts @@ -1,7 +1,7 @@ import { chunk } from 'lodash' import { doJSONRequest } from '@server/helpers/requests' import { JobQueue } from '@server/lib/job-queue' -import { ActorFollowModel } from '@server/models/activitypub/actor-follow' +import { ActorFollowModel } from '@server/models/actor/actor-follow' import { getServerActor } from '@server/models/application/application' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' diff --git a/server/lib/schedulers/remove-old-history-scheduler.ts b/server/lib/schedulers/remove-old-history-scheduler.ts index 17a42b2c4..225669ea2 100644 --- a/server/lib/schedulers/remove-old-history-scheduler.ts +++ b/server/lib/schedulers/remove-old-history-scheduler.ts @@ -1,7 +1,7 @@ import { logger } from '../../helpers/logger' import { AbstractScheduler } from './abstract-scheduler' import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' -import { UserVideoHistoryModel } from '../../models/account/user-video-history' +import { UserVideoHistoryModel } from '../../models/user/user-video-history' import { CONFIG } from '../../initializers/config' export class RemoveOldHistoryScheduler extends AbstractScheduler { diff --git a/server/lib/stat-manager.ts b/server/lib/stat-manager.ts index 09ba208bd..25ed21927 100644 --- a/server/lib/stat-manager.ts +++ b/server/lib/stat-manager.ts @@ -1,6 +1,6 @@ import { CONFIG } from '@server/initializers/config' -import { UserModel } from '@server/models/account/user' -import { ActorFollowModel } from '@server/models/activitypub/actor-follow' +import { UserModel } from '@server/models/user/user' +import { ActorFollowModel } from '@server/models/actor/actor-follow' import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' import { VideoModel } from '@server/models/video/video' import { VideoChannelModel } from '@server/models/video/video-channel' diff --git a/server/lib/user.ts b/server/lib/user.ts index 9b0a0a2f1..8a6fcebc7 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -1,14 +1,15 @@ import { Transaction } from 'sequelize/types' import { v4 as uuidv4 } from 'uuid' -import { UserModel } from '@server/models/account/user' +import { UserModel } from '@server/models/user/user' +import { MActorDefault } from '@server/types/models/actor' import { ActivityPubActorType } from '../../shared/models/activitypub' import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' import { sequelizeTypescript } from '../initializers/database' import { AccountModel } from '../models/account/account' -import { UserNotificationSettingModel } from '../models/account/user-notification-setting' -import { ActorModel } from '../models/activitypub/actor' -import { MAccountDefault, MActorDefault, MChannelActor } from '../types/models' +import { ActorModel } from '../models/actor/actor' +import { UserNotificationSettingModel } from '../models/user/user-notification-setting' +import { MAccountDefault, MChannelActor } from '../types/models' import { MUser, MUserDefault, MUserId } from '../types/models/user' import { buildActorInstance, generateAndSaveActorKeys } from './activitypub/actor' import { getLocalAccountActivityPubUrl } from './activitypub/url' diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index bb849dc72..1d18de8cd 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -1,18 +1,18 @@ import * as express from 'express' import { body, param, query } from 'express-validator' +import { isFollowStateValid } from '@server/helpers/custom-validators/follows' +import { getServerActor } from '@server/models/application/application' +import { MActorFollowActorsDefault } from '@server/types/models' +import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { isTestInstance } from '../../helpers/core-utils' +import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' import { logger } from '../../helpers/logger' +import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' -import { ActorFollowModel } from '../../models/activitypub/actor-follow' +import { ActorModel } from '../../models/actor/actor' +import { ActorFollowModel } from '../../models/actor/actor-follow' import { areValidationErrors } from './utils' -import { ActorModel } from '../../models/activitypub/actor' -import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' -import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' -import { MActorFollowActorsDefault } from '@server/types/models' -import { isFollowStateValid } from '@server/helpers/custom-validators/follows' -import { getServerActor } from '@server/models/application/application' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' const listFollowsValidator = [ query('state') diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts index 0d0c8ccbf..1823892b6 100644 --- a/server/middlewares/validators/user-subscriptions.ts +++ b/server/middlewares/validators/user-subscriptions.ts @@ -1,12 +1,12 @@ import * as express from 'express' import { body, param, query } from 'express-validator' -import { logger } from '../../helpers/logger' -import { areValidationErrors } from './utils' -import { ActorFollowModel } from '../../models/activitypub/actor-follow' +import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' import { toArray } from '../../helpers/custom-validators/misc' +import { logger } from '../../helpers/logger' import { WEBSERVER } from '../../initializers/constants' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' +import { ActorFollowModel } from '../../models/actor/actor-follow' +import { areValidationErrors } from './utils' const userSubscriptionListValidator = [ query('search').optional().not().isEmpty().withMessage('Should have a valid search'), diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 37119e279..548d5df4d 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -34,8 +34,8 @@ import { doesVideoExist } from '../../helpers/middlewares' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' import { isThemeRegistered } from '../../lib/plugins/theme-utils' import { Redis } from '../../lib/redis' -import { UserModel } from '../../models/account/user' -import { ActorModel } from '../../models/activitypub/actor' +import { UserModel } from '../../models/user/user' +import { ActorModel } from '../../models/actor/actor' import { areValidationErrors } from './utils' const usersListValidator = [ diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 2463d281c..e881f0d3e 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts @@ -3,6 +3,7 @@ import { body, param, query } from 'express-validator' import { VIDEO_CHANNELS } from '@server/initializers/constants' import { MChannelAccountDefault, MUser } from '@server/types/models' import { UserRight } from '../../../../shared' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' import { @@ -12,10 +13,9 @@ import { } from '../../../helpers/custom-validators/video-channels' import { logger } from '../../../helpers/logger' import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares' -import { ActorModel } from '../../../models/activitypub/actor' +import { ActorModel } from '../../../models/actor/actor' import { VideoChannelModel } from '../../../models/video/video-channel' import { areValidationErrors } from '../utils' -import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' const videoChannelsAddValidator = [ body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index a71422ed8..c2dfccc96 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts @@ -1,11 +1,11 @@ import * as express from 'express' import { query } from 'express-validator' +import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' +import { getHostWithPort } from '../../helpers/express-utils' import { logger } from '../../helpers/logger' -import { ActorModel } from '../../models/activitypub/actor' +import { ActorModel } from '../../models/actor/actor' import { areValidationErrors } from './utils' -import { getHostWithPort } from '../../helpers/express-utils' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' const webfingerValidator = [ query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'), diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fe9168ab8..9f3be22bd 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -2,7 +2,7 @@ import { Op } from 'sequelize' import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' import { AccountBlock } from '../../../shared/models' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' import { ServerModel } from '../server/server' import { getSort, searchAttribute } from '../utils' import { AccountModel } from './account' diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 801f76bba..576a44576 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -11,7 +11,7 @@ import { AccountVideoRate } from '../../../shared' import { VideoRateType } from '../../../shared/models/videos' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' import { VideoModel } from '../video/video' import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d33353af7..7b2af706d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -30,19 +30,19 @@ import { MAccountSummaryFormattable, MChannelActor } from '../../types/models' -import { ActorModel } from '../activitypub/actor' -import { ActorFollowModel } from '../activitypub/actor-follow' +import { ActorModel } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' import { ApplicationModel } from '../application/application' -import { ActorImageModel } from './actor-image' import { ServerModel } from '../server/server' import { ServerBlocklistModel } from '../server/server-blocklist' +import { UserModel } from '../user/user' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { VideoCommentModel } from '../video/video-comment' import { VideoPlaylistModel } from '../video/video-playlist' import { AccountBlocklistModel } from './account-blocklist' -import { UserModel } from './user' export enum ScopeNames { SUMMARY = 'SUMMARY' diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts deleted file mode 100644 index ae05b4969..000000000 --- a/server/models/account/actor-image.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { remove } from 'fs-extra' -import { join } from 'path' -import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' -import { MActorImageFormattable } from '@server/types/models' -import { ActorImageType } from '@shared/models' -import { ActorImage } from '../../../shared/models/actors/actor-image.model' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { logger } from '../../helpers/logger' -import { CONFIG } from '../../initializers/config' -import { LAZY_STATIC_PATHS } from '../../initializers/constants' -import { throwIfNotValid } from '../utils' - -@Table({ - tableName: 'actorImage', - indexes: [ - { - fields: [ 'filename' ], - unique: true - } - ] -}) -export class ActorImageModel extends Model { - - @AllowNull(false) - @Column - filename: string - - @AllowNull(true) - @Default(null) - @Column - height: number - - @AllowNull(true) - @Default(null) - @Column - width: number - - @AllowNull(true) - @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) - @Column - fileUrl: string - - @AllowNull(false) - @Column - onDisk: boolean - - @AllowNull(false) - @Column - type: ActorImageType - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @AfterDestroy - static removeFilesAndSendDelete (instance: ActorImageModel) { - logger.info('Removing actor image file %s.', instance.filename) - - // Don't block the transaction - instance.removeImage() - .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) - } - - static loadByName (filename: string) { - const query = { - where: { - filename - } - } - - return ActorImageModel.findOne(query) - } - - toFormattedJSON (this: MActorImageFormattable): ActorImage { - return { - path: this.getStaticPath(), - createdAt: this.createdAt, - updatedAt: this.updatedAt - } - } - - getStaticPath () { - if (this.type === ActorImageType.AVATAR) { - return join(LAZY_STATIC_PATHS.AVATARS, this.filename) - } - - return join(LAZY_STATIC_PATHS.BANNERS, this.filename) - } - - getPath () { - return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) - } - - removeImage () { - const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) - return remove(imagePath) - } -} diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts deleted file mode 100644 index 138051528..000000000 --- a/server/models/account/user-notification-setting.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - AfterDestroy, - AfterUpdate, - AllowNull, - BelongsTo, - Column, - CreatedAt, - Default, - ForeignKey, - Is, - Model, - Table, - UpdatedAt -} from 'sequelize-typescript' -import { TokensCache } from '@server/lib/auth/tokens-cache' -import { MNotificationSettingFormattable } from '@server/types/models' -import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' -import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' -import { throwIfNotValid } from '../utils' -import { UserModel } from './user' - -@Table({ - tableName: 'userNotificationSetting', - indexes: [ - { - fields: [ 'userId' ], - unique: true - } - ] -}) -export class UserNotificationSettingModel extends Model { - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewVideoFromSubscription', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') - ) - @Column - newVideoFromSubscription: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewCommentOnMyVideo', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') - ) - @Column - newCommentOnMyVideo: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingAbuseAsModerator', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator') - ) - @Column - abuseAsModerator: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingVideoAutoBlacklistAsModerator', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator') - ) - @Column - videoAutoBlacklistAsModerator: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingBlacklistOnMyVideo', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') - ) - @Column - blacklistOnMyVideo: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingMyVideoPublished', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') - ) - @Column - myVideoPublished: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingMyVideoImportFinished', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') - ) - @Column - myVideoImportFinished: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewUserRegistration', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') - ) - @Column - newUserRegistration: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewInstanceFollower', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower') - ) - @Column - newInstanceFollower: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewInstanceFollower', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing') - ) - @Column - autoInstanceFollowing: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewFollow', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') - ) - @Column - newFollow: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingCommentMention', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') - ) - @Column - commentMention: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingAbuseStateChange', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange') - ) - @Column - abuseStateChange: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingAbuseNewMessage', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage') - ) - @Column - abuseNewMessage: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewPeerTubeVersion', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion') - ) - @Column - newPeerTubeVersion: UserNotificationSettingValue - - @AllowNull(false) - @Default(null) - @Is( - 'UserNotificationSettingNewPeerPluginVersion', - value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion') - ) - @Column - newPluginVersion: UserNotificationSettingValue - - @ForeignKey(() => UserModel) - @Column - userId: number - - @BelongsTo(() => UserModel, { - foreignKey: { - allowNull: false - }, - onDelete: 'cascade' - }) - User: UserModel - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @AfterUpdate - @AfterDestroy - static removeTokenCache (instance: UserNotificationSettingModel) { - return TokensCache.Instance.clearCacheByUserId(instance.userId) - } - - toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { - return { - newCommentOnMyVideo: this.newCommentOnMyVideo, - newVideoFromSubscription: this.newVideoFromSubscription, - abuseAsModerator: this.abuseAsModerator, - videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, - blacklistOnMyVideo: this.blacklistOnMyVideo, - myVideoPublished: this.myVideoPublished, - myVideoImportFinished: this.myVideoImportFinished, - newUserRegistration: this.newUserRegistration, - commentMention: this.commentMention, - newFollow: this.newFollow, - newInstanceFollower: this.newInstanceFollower, - autoInstanceFollowing: this.autoInstanceFollowing, - abuseNewMessage: this.abuseNewMessage, - abuseStateChange: this.abuseStateChange, - newPeerTubeVersion: this.newPeerTubeVersion, - newPluginVersion: this.newPluginVersion - } - } -} diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts deleted file mode 100644 index 805095002..000000000 --- a/server/models/account/user-notification.ts +++ /dev/null @@ -1,665 +0,0 @@ -import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' -import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' -import { UserNotification, UserNotificationType } from '../../../shared' -import { isBooleanValid } from '../../helpers/custom-validators/misc' -import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' -import { AbuseModel } from '../abuse/abuse' -import { VideoAbuseModel } from '../abuse/video-abuse' -import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' -import { ActorModel } from '../activitypub/actor' -import { ActorFollowModel } from '../activitypub/actor-follow' -import { ApplicationModel } from '../application/application' -import { PluginModel } from '../server/plugin' -import { ServerModel } from '../server/server' -import { getSort, throwIfNotValid } from '../utils' -import { VideoModel } from '../video/video' -import { VideoBlacklistModel } from '../video/video-blacklist' -import { VideoChannelModel } from '../video/video-channel' -import { VideoCommentModel } from '../video/video-comment' -import { VideoImportModel } from '../video/video-import' -import { AccountModel } from './account' -import { ActorImageModel } from './actor-image' -import { UserModel } from './user' - -enum ScopeNames { - WITH_ALL = 'WITH_ALL' -} - -function buildActorWithAvatarInclude () { - return { - attributes: [ 'preferredUsername' ], - model: ActorModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'filename' ], - as: 'Avatar', - model: ActorImageModel.unscoped(), - required: false - }, - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - } - ] - } -} - -function buildVideoInclude (required: boolean) { - return { - attributes: [ 'id', 'uuid', 'name' ], - model: VideoModel.unscoped(), - required - } -} - -function buildChannelInclude (required: boolean, withActor = false) { - return { - required, - attributes: [ 'id', 'name' ], - model: VideoChannelModel.unscoped(), - include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] - } -} - -function buildAccountInclude (required: boolean, withActor = false) { - return { - required, - attributes: [ 'id', 'name' ], - model: AccountModel.unscoped(), - include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] - } -} - -@Scopes(() => ({ - [ScopeNames.WITH_ALL]: { - include: [ - Object.assign(buildVideoInclude(false), { - include: [ buildChannelInclude(true, true) ] - }), - - { - attributes: [ 'id', 'originCommentId' ], - model: VideoCommentModel.unscoped(), - required: false, - include: [ - buildAccountInclude(true, true), - buildVideoInclude(true) - ] - }, - - { - attributes: [ 'id', 'state' ], - model: AbuseModel.unscoped(), - required: false, - include: [ - { - attributes: [ 'id' ], - model: VideoAbuseModel.unscoped(), - required: false, - include: [ buildVideoInclude(false) ] - }, - { - attributes: [ 'id' ], - model: VideoCommentAbuseModel.unscoped(), - required: false, - include: [ - { - attributes: [ 'id', 'originCommentId' ], - model: VideoCommentModel.unscoped(), - required: false, - include: [ - { - attributes: [ 'id', 'name', 'uuid' ], - model: VideoModel.unscoped(), - required: false - } - ] - } - ] - }, - { - model: AccountModel, - as: 'FlaggedAccount', - required: false, - include: [ buildActorWithAvatarInclude() ] - } - ] - }, - - { - attributes: [ 'id' ], - model: VideoBlacklistModel.unscoped(), - required: false, - include: [ buildVideoInclude(true) ] - }, - - { - attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], - model: VideoImportModel.unscoped(), - required: false, - include: [ buildVideoInclude(false) ] - }, - - { - attributes: [ 'id', 'name', 'type', 'latestVersion' ], - model: PluginModel.unscoped(), - required: false - }, - - { - attributes: [ 'id', 'latestPeerTubeVersion' ], - model: ApplicationModel.unscoped(), - required: false - }, - - { - attributes: [ 'id', 'state' ], - model: ActorFollowModel.unscoped(), - required: false, - include: [ - { - attributes: [ 'preferredUsername' ], - model: ActorModel.unscoped(), - required: true, - as: 'ActorFollower', - include: [ - { - attributes: [ 'id', 'name' ], - model: AccountModel.unscoped(), - required: true - }, - { - attributes: [ 'filename' ], - as: 'Avatar', - model: ActorImageModel.unscoped(), - required: false - }, - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - } - ] - }, - { - attributes: [ 'preferredUsername', 'type' ], - model: ActorModel.unscoped(), - required: true, - as: 'ActorFollowing', - include: [ - buildChannelInclude(false), - buildAccountInclude(false), - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - } - ] - } - ] - }, - - buildAccountInclude(false, true) - ] - } -})) -@Table({ - tableName: 'userNotification', - indexes: [ - { - fields: [ 'userId' ] - }, - { - fields: [ 'videoId' ], - where: { - videoId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'commentId' ], - where: { - commentId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'abuseId' ], - where: { - abuseId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'videoBlacklistId' ], - where: { - videoBlacklistId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'videoImportId' ], - where: { - videoImportId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'accountId' ], - where: { - accountId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'actorFollowId' ], - where: { - actorFollowId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'pluginId' ], - where: { - pluginId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'applicationId' ], - where: { - applicationId: { - [Op.ne]: null - } - } - } - ] as (ModelIndexesOptions & { where?: WhereOptions })[] -}) -export class UserNotificationModel extends Model { - - @AllowNull(false) - @Default(null) - @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) - @Column - type: UserNotificationType - - @AllowNull(false) - @Default(false) - @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) - @Column - read: boolean - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @ForeignKey(() => UserModel) - @Column - userId: number - - @BelongsTo(() => UserModel, { - foreignKey: { - allowNull: false - }, - onDelete: 'cascade' - }) - User: UserModel - - @ForeignKey(() => VideoModel) - @Column - videoId: number - - @BelongsTo(() => VideoModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Video: VideoModel - - @ForeignKey(() => VideoCommentModel) - @Column - commentId: number - - @BelongsTo(() => VideoCommentModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Comment: VideoCommentModel - - @ForeignKey(() => AbuseModel) - @Column - abuseId: number - - @BelongsTo(() => AbuseModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Abuse: AbuseModel - - @ForeignKey(() => VideoBlacklistModel) - @Column - videoBlacklistId: number - - @BelongsTo(() => VideoBlacklistModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - VideoBlacklist: VideoBlacklistModel - - @ForeignKey(() => VideoImportModel) - @Column - videoImportId: number - - @BelongsTo(() => VideoImportModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - VideoImport: VideoImportModel - - @ForeignKey(() => AccountModel) - @Column - accountId: number - - @BelongsTo(() => AccountModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Account: AccountModel - - @ForeignKey(() => ActorFollowModel) - @Column - actorFollowId: number - - @BelongsTo(() => ActorFollowModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - ActorFollow: ActorFollowModel - - @ForeignKey(() => PluginModel) - @Column - pluginId: number - - @BelongsTo(() => PluginModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Plugin: PluginModel - - @ForeignKey(() => ApplicationModel) - @Column - applicationId: number - - @BelongsTo(() => ApplicationModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Application: ApplicationModel - - static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { - const where = { userId } - - const query: FindOptions = { - offset: start, - limit: count, - order: getSort(sort), - where - } - - if (unread !== undefined) query.where['read'] = !unread - - return Promise.all([ - UserNotificationModel.count({ where }) - .then(count => count || 0), - - count === 0 - ? [] - : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) - ]).then(([ total, data ]) => ({ total, data })) - } - - static markAsRead (userId: number, notificationIds: number[]) { - const query = { - where: { - userId, - id: { - [Op.in]: notificationIds - } - } - } - - return UserNotificationModel.update({ read: true }, query) - } - - static markAllAsRead (userId: number) { - const query = { where: { userId } } - - return UserNotificationModel.update({ read: true }, query) - } - - static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) { - const id = parseInt(options.id + '', 10) - - function buildAccountWhereQuery (base: string) { - const whereSuffix = options.forUserId - ? ` AND "userNotification"."userId" = ${options.forUserId}` - : '' - - if (options.type === 'account') { - return base + - ` WHERE "account"."id" = ${id} ${whereSuffix}` - } - - return base + - ` WHERE "actor"."serverId" = ${id} ${whereSuffix}` - } - - const queries = [ - buildAccountWhereQuery( - `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + - `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` - ), - - // Remove notifications from muted accounts that followed ours - buildAccountWhereQuery( - `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + - `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + - `INNER JOIN account ON account."actorId" = actor.id ` - ), - - // Remove notifications from muted accounts that commented something - buildAccountWhereQuery( - `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + - `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + - `INNER JOIN account ON account."actorId" = actor.id ` - ), - - buildAccountWhereQuery( - `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + - `INNER JOIN account ON account.id = "videoComment"."accountId" ` + - `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` - ) - ] - - const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})` - - return UserNotificationModel.sequelize.query(query) - } - - toFormattedJSON (this: UserNotificationModelForApi): UserNotification { - const video = this.Video - ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) - : undefined - - const videoImport = this.VideoImport - ? { - id: this.VideoImport.id, - video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, - torrentName: this.VideoImport.torrentName, - magnetUri: this.VideoImport.magnetUri, - targetUrl: this.VideoImport.targetUrl - } - : undefined - - const comment = this.Comment - ? { - id: this.Comment.id, - threadId: this.Comment.getThreadId(), - account: this.formatActor(this.Comment.Account), - video: this.formatVideo(this.Comment.Video) - } - : undefined - - const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined - - const videoBlacklist = this.VideoBlacklist - ? { - id: this.VideoBlacklist.id, - video: this.formatVideo(this.VideoBlacklist.Video) - } - : undefined - - const account = this.Account ? this.formatActor(this.Account) : undefined - - const actorFollowingType = { - Application: 'instance' as 'instance', - Group: 'channel' as 'channel', - Person: 'account' as 'account' - } - const actorFollow = this.ActorFollow - ? { - id: this.ActorFollow.id, - state: this.ActorFollow.state, - follower: { - id: this.ActorFollow.ActorFollower.Account.id, - displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), - name: this.ActorFollow.ActorFollower.preferredUsername, - avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, - host: this.ActorFollow.ActorFollower.getHost() - }, - following: { - type: actorFollowingType[this.ActorFollow.ActorFollowing.type], - displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), - name: this.ActorFollow.ActorFollowing.preferredUsername, - host: this.ActorFollow.ActorFollowing.getHost() - } - } - : undefined - - const plugin = this.Plugin - ? { - name: this.Plugin.name, - type: this.Plugin.type, - latestVersion: this.Plugin.latestVersion - } - : undefined - - const peertube = this.Application - ? { latestVersion: this.Application.latestPeerTubeVersion } - : undefined - - return { - id: this.id, - type: this.type, - read: this.read, - video, - videoImport, - comment, - abuse, - videoBlacklist, - account, - actorFollow, - plugin, - peertube, - createdAt: this.createdAt.toISOString(), - updatedAt: this.updatedAt.toISOString() - } - } - - formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { - return { - id: video.id, - uuid: video.uuid, - name: video.name - } - } - - formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { - const commentAbuse = abuse.VideoCommentAbuse?.VideoComment - ? { - threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), - - video: abuse.VideoCommentAbuse.VideoComment.Video - ? { - id: abuse.VideoCommentAbuse.VideoComment.Video.id, - name: abuse.VideoCommentAbuse.VideoComment.Video.name, - uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid - } - : undefined - } - : undefined - - const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined - - const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined - - return { - id: abuse.id, - state: abuse.state, - video: videoAbuse, - comment: commentAbuse, - account: accountAbuse - } - } - - formatActor ( - this: UserNotificationModelForApi, - accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor - ) { - const avatar = accountOrChannel.Actor.Avatar - ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } - : undefined - - return { - id: accountOrChannel.id, - displayName: accountOrChannel.getDisplayName(), - name: accountOrChannel.Actor.preferredUsername, - host: accountOrChannel.Actor.getHost(), - avatar - } - } -} diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts deleted file mode 100644 index 6be1d65ea..000000000 --- a/server/models/account/user-video-history.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' -import { VideoModel } from '../video/video' -import { UserModel } from './user' -import { DestroyOptions, Op, Transaction } from 'sequelize' -import { MUserAccountId, MUserId } from '@server/types/models' - -@Table({ - tableName: 'userVideoHistory', - indexes: [ - { - fields: [ 'userId', 'videoId' ], - unique: true - }, - { - fields: [ 'userId' ] - }, - { - fields: [ 'videoId' ] - } - ] -}) -export class UserVideoHistoryModel extends Model { - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @AllowNull(false) - @IsInt - @Column - currentTime: number - - @ForeignKey(() => VideoModel) - @Column - videoId: number - - @BelongsTo(() => VideoModel, { - foreignKey: { - allowNull: false - }, - onDelete: 'CASCADE' - }) - Video: VideoModel - - @ForeignKey(() => UserModel) - @Column - userId: number - - @BelongsTo(() => UserModel, { - foreignKey: { - allowNull: false - }, - onDelete: 'CASCADE' - }) - User: UserModel - - static listForApi (user: MUserAccountId, start: number, count: number, search?: string) { - return VideoModel.listForApi({ - start, - count, - search, - sort: '-"userVideoHistory"."updatedAt"', - nsfw: null, // All - includeLocalVideos: true, - withFiles: false, - user, - historyOfUser: user - }) - } - - static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) { - const query: DestroyOptions = { - where: { - userId: user.id - }, - transaction: t - } - - if (beforeDate) { - query.where['updatedAt'] = { - [Op.lt]: beforeDate - } - } - - return UserVideoHistoryModel.destroy(query) - } - - static removeOldHistory (beforeDate: string) { - const query: DestroyOptions = { - where: { - updatedAt: { - [Op.lt]: beforeDate - } - } - } - - return UserVideoHistoryModel.destroy(query) - } -} diff --git a/server/models/account/user.ts b/server/models/account/user.ts deleted file mode 100644 index 513455773..000000000 --- a/server/models/account/user.ts +++ /dev/null @@ -1,967 +0,0 @@ -import { values } from 'lodash' -import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize' -import { - AfterDestroy, - AfterUpdate, - AllowNull, - BeforeCreate, - BeforeUpdate, - Column, - CreatedAt, - DataType, - Default, - DefaultScope, - HasMany, - HasOne, - Is, - IsEmail, - IsUUID, - Model, - Scopes, - Table, - UpdatedAt -} from 'sequelize-typescript' -import { TokensCache } from '@server/lib/auth/tokens-cache' -import { - MMyUserFormattable, - MUser, - MUserDefault, - MUserFormattable, - MUserNotifSettingChannelDefault, - MUserWithNotificationSetting, - MVideoWithRights -} from '@server/types/models' -import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' -import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' -import { User, UserRole } from '../../../shared/models/users' -import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' -import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' -import { isThemeNameValid } from '../../helpers/custom-validators/plugins' -import { - isNoInstanceConfigWarningModal, - isNoWelcomeModal, - isUserAdminFlagsValid, - isUserAutoPlayNextVideoPlaylistValid, - isUserAutoPlayNextVideoValid, - isUserAutoPlayVideoValid, - isUserBlockedReasonValid, - isUserBlockedValid, - isUserEmailVerifiedValid, - isUserNSFWPolicyValid, - isUserPasswordValid, - isUserRoleValid, - isUserUsernameValid, - isUserVideoLanguages, - isUserVideoQuotaDailyValid, - isUserVideoQuotaValid, - isUserVideosHistoryEnabledValid, - isUserWebTorrentEnabledValid -} from '../../helpers/custom-validators/users' -import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' -import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' -import { getThemeOrDefault } from '../../lib/plugins/theme-utils' -import { ActorModel } from '../activitypub/actor' -import { ActorFollowModel } from '../activitypub/actor-follow' -import { OAuthTokenModel } from '../oauth/oauth-token' -import { getSort, throwIfNotValid } from '../utils' -import { VideoModel } from '../video/video' -import { VideoChannelModel } from '../video/video-channel' -import { VideoImportModel } from '../video/video-import' -import { VideoLiveModel } from '../video/video-live' -import { VideoPlaylistModel } from '../video/video-playlist' -import { AccountModel } from './account' -import { UserNotificationSettingModel } from './user-notification-setting' -import { ActorImageModel } from './actor-image' - -enum ScopeNames { - FOR_ME_API = 'FOR_ME_API', - WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', - WITH_STATS = 'WITH_STATS' -} - -@DefaultScope(() => ({ - include: [ - { - model: AccountModel, - required: true - }, - { - model: UserNotificationSettingModel, - required: true - } - ] -})) -@Scopes(() => ({ - [ScopeNames.FOR_ME_API]: { - include: [ - { - model: AccountModel, - include: [ - { - model: VideoChannelModel.unscoped(), - include: [ - { - model: ActorModel, - required: true, - include: [ - { - model: ActorImageModel, - as: 'Banner', - required: false - } - ] - } - ] - }, - { - attributes: [ 'id', 'name', 'type' ], - model: VideoPlaylistModel.unscoped(), - required: true, - where: { - type: { - [Op.ne]: VideoPlaylistType.REGULAR - } - } - } - ] - }, - { - model: UserNotificationSettingModel, - required: true - } - ] - }, - [ScopeNames.WITH_VIDEOCHANNELS]: { - include: [ - { - model: AccountModel, - include: [ - { - model: VideoChannelModel - }, - { - attributes: [ 'id', 'name', 'type' ], - model: VideoPlaylistModel.unscoped(), - required: true, - where: { - type: { - [Op.ne]: VideoPlaylistType.REGULAR - } - } - } - ] - } - ] - }, - [ScopeNames.WITH_STATS]: { - attributes: { - include: [ - [ - literal( - '(' + - UserModel.generateUserQuotaBaseSQL({ - withSelect: false, - whereUserId: '"UserModel"."id"' - }) + - ')' - ), - 'videoQuotaUsed' - ], - [ - literal( - '(' + - 'SELECT COUNT("video"."id") ' + - 'FROM "video" ' + - 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + - 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + - 'WHERE "account"."userId" = "UserModel"."id"' + - ')' - ), - 'videosCount' - ], - [ - literal( - '(' + - `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + - 'FROM (' + - 'SELECT COUNT("abuse"."id") AS "abuses", ' + - `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + - 'FROM "abuse" ' + - 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + - 'WHERE "account"."userId" = "UserModel"."id"' + - ') t' + - ')' - ), - 'abusesCount' - ], - [ - literal( - '(' + - 'SELECT COUNT("abuse"."id") ' + - 'FROM "abuse" ' + - 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + - 'WHERE "account"."userId" = "UserModel"."id"' + - ')' - ), - 'abusesCreatedCount' - ], - [ - literal( - '(' + - 'SELECT COUNT("videoComment"."id") ' + - 'FROM "videoComment" ' + - 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' + - 'WHERE "account"."userId" = "UserModel"."id"' + - ')' - ), - 'videoCommentsCount' - ] - ] - } - } -})) -@Table({ - tableName: 'user', - indexes: [ - { - fields: [ 'username' ], - unique: true - }, - { - fields: [ 'email' ], - unique: true - } - ] -}) -export class UserModel extends Model { - - @AllowNull(true) - @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) - @Column - password: string - - @AllowNull(false) - @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name')) - @Column - username: string - - @AllowNull(false) - @IsEmail - @Column(DataType.STRING(400)) - email: string - - @AllowNull(true) - @IsEmail - @Column(DataType.STRING(400)) - pendingEmail: string - - @AllowNull(true) - @Default(null) - @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) - @Column - emailVerified: boolean - - @AllowNull(false) - @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) - @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES))) - nsfwPolicy: NSFWPolicyType - - @AllowNull(false) - @Default(true) - @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) - @Column - webTorrentEnabled: boolean - - @AllowNull(false) - @Default(true) - @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) - @Column - videosHistoryEnabled: boolean - - @AllowNull(false) - @Default(true) - @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) - @Column - autoPlayVideo: boolean - - @AllowNull(false) - @Default(false) - @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean')) - @Column - autoPlayNextVideo: boolean - - @AllowNull(false) - @Default(true) - @Is( - 'UserAutoPlayNextVideoPlaylist', - value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean') - ) - @Column - autoPlayNextVideoPlaylist: boolean - - @AllowNull(true) - @Default(null) - @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages')) - @Column(DataType.ARRAY(DataType.STRING)) - videoLanguages: string[] - - @AllowNull(false) - @Default(UserAdminFlag.NONE) - @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags')) - @Column - adminFlags?: UserAdminFlag - - @AllowNull(false) - @Default(false) - @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean')) - @Column - blocked: boolean - - @AllowNull(true) - @Default(null) - @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true)) - @Column - blockedReason: string - - @AllowNull(false) - @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role')) - @Column - role: number - - @AllowNull(false) - @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota')) - @Column(DataType.BIGINT) - videoQuota: number - - @AllowNull(false) - @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily')) - @Column(DataType.BIGINT) - videoQuotaDaily: number - - @AllowNull(false) - @Default(DEFAULT_USER_THEME_NAME) - @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme')) - @Column - theme: string - - @AllowNull(false) - @Default(false) - @Is( - 'UserNoInstanceConfigWarningModal', - value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal') - ) - @Column - noInstanceConfigWarningModal: boolean - - @AllowNull(false) - @Default(false) - @Is( - 'UserNoInstanceConfigWarningModal', - value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal') - ) - @Column - noWelcomeModal: boolean - - @AllowNull(true) - @Default(null) - @Column - pluginAuth: string - - @AllowNull(false) - @Default(DataType.UUIDV4) - @IsUUID(4) - @Column(DataType.UUID) - feedToken: string - - @AllowNull(true) - @Default(null) - @Column - lastLoginDate: Date - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @HasOne(() => AccountModel, { - foreignKey: 'userId', - onDelete: 'cascade', - hooks: true - }) - Account: AccountModel - - @HasOne(() => UserNotificationSettingModel, { - foreignKey: 'userId', - onDelete: 'cascade', - hooks: true - }) - NotificationSetting: UserNotificationSettingModel - - @HasMany(() => VideoImportModel, { - foreignKey: 'userId', - onDelete: 'cascade' - }) - VideoImports: VideoImportModel[] - - @HasMany(() => OAuthTokenModel, { - foreignKey: 'userId', - onDelete: 'cascade' - }) - OAuthTokens: OAuthTokenModel[] - - @BeforeCreate - @BeforeUpdate - static cryptPasswordIfNeeded (instance: UserModel) { - if (instance.changed('password') && instance.password) { - return cryptPassword(instance.password) - .then(hash => { - instance.password = hash - return undefined - }) - } - } - - @AfterUpdate - @AfterDestroy - static removeTokenCache (instance: UserModel) { - return TokensCache.Instance.clearCacheByUserId(instance.id) - } - - static countTotal () { - return this.count() - } - - static listForApi (parameters: { - start: number - count: number - sort: string - search?: string - blocked?: boolean - }) { - const { start, count, sort, search, blocked } = parameters - const where: WhereOptions = {} - - if (search) { - Object.assign(where, { - [Op.or]: [ - { - email: { - [Op.iLike]: '%' + search + '%' - } - }, - { - username: { - [Op.iLike]: '%' + search + '%' - } - } - ] - }) - } - - if (blocked !== undefined) { - Object.assign(where, { - blocked: blocked - }) - } - - const query: FindOptions = { - attributes: { - include: [ - [ - literal( - '(' + - UserModel.generateUserQuotaBaseSQL({ - withSelect: false, - whereUserId: '"UserModel"."id"' - }) + - ')' - ), - 'videoQuotaUsed' - ] as any // FIXME: typings - ] - }, - offset: start, - limit: count, - order: getSort(sort), - where - } - - return UserModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) - } - - static listWithRight (right: UserRight): Promise { - const roles = Object.keys(USER_ROLE_LABELS) - .map(k => parseInt(k, 10) as UserRole) - .filter(role => hasUserRight(role, right)) - - const query = { - where: { - role: { - [Op.in]: roles - } - } - } - - return UserModel.findAll(query) - } - - static listUserSubscribersOf (actorId: number): Promise { - const query = { - include: [ - { - model: UserNotificationSettingModel.unscoped(), - required: true - }, - { - attributes: [ 'userId' ], - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: [], - model: ActorModel.unscoped(), - required: true, - where: { - serverId: null - }, - include: [ - { - attributes: [], - as: 'ActorFollowings', - model: ActorFollowModel.unscoped(), - required: true, - where: { - targetActorId: actorId - } - } - ] - } - ] - } - ] - } - - return UserModel.unscoped().findAll(query) - } - - static listByUsernames (usernames: string[]): Promise { - const query = { - where: { - username: usernames - } - } - - return UserModel.findAll(query) - } - - static loadById (id: number): Promise { - return UserModel.unscoped().findByPk(id) - } - - static loadByIdFull (id: number): Promise { - return UserModel.findByPk(id) - } - - static loadByIdWithChannels (id: number, withStats = false): Promise { - const scopes = [ - ScopeNames.WITH_VIDEOCHANNELS - ] - - if (withStats) scopes.push(ScopeNames.WITH_STATS) - - return UserModel.scope(scopes).findByPk(id) - } - - static loadByUsername (username: string): Promise { - const query = { - where: { - username - } - } - - return UserModel.findOne(query) - } - - static loadForMeAPI (id: number): Promise { - const query = { - where: { - id - } - } - - return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query) - } - - static loadByEmail (email: string): Promise { - const query = { - where: { - email - } - } - - return UserModel.findOne(query) - } - - static loadByUsernameOrEmail (username: string, email?: string): Promise { - if (!email) email = username - - const query = { - where: { - [Op.or]: [ - where(fn('lower', col('username')), fn('lower', username)), - - { email } - ] - } - } - - return UserModel.findOne(query) - } - - static loadByVideoId (videoId: number): Promise { - const query = { - include: [ - { - required: true, - attributes: [ 'id' ], - model: AccountModel.unscoped(), - include: [ - { - required: true, - attributes: [ 'id' ], - model: VideoChannelModel.unscoped(), - include: [ - { - required: true, - attributes: [ 'id' ], - model: VideoModel.unscoped(), - where: { - id: videoId - } - } - ] - } - ] - } - ] - } - - return UserModel.findOne(query) - } - - static loadByVideoImportId (videoImportId: number): Promise { - const query = { - include: [ - { - required: true, - attributes: [ 'id' ], - model: VideoImportModel.unscoped(), - where: { - id: videoImportId - } - } - ] - } - - return UserModel.findOne(query) - } - - static loadByChannelActorId (videoChannelActorId: number): Promise { - const query = { - include: [ - { - required: true, - attributes: [ 'id' ], - model: AccountModel.unscoped(), - include: [ - { - required: true, - attributes: [ 'id' ], - model: VideoChannelModel.unscoped(), - where: { - actorId: videoChannelActorId - } - } - ] - } - ] - } - - return UserModel.findOne(query) - } - - static loadByAccountActorId (accountActorId: number): Promise { - const query = { - include: [ - { - required: true, - attributes: [ 'id' ], - model: AccountModel.unscoped(), - where: { - actorId: accountActorId - } - } - ] - } - - return UserModel.findOne(query) - } - - static loadByLiveId (liveId: number): Promise { - const query = { - include: [ - { - attributes: [ 'id' ], - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id' ], - model: VideoModel.unscoped(), - required: true, - include: [ - { - attributes: [], - model: VideoLiveModel.unscoped(), - required: true, - where: { - id: liveId - } - } - ] - } - ] - } - ] - } - ] - } - - return UserModel.unscoped().findOne(query) - } - - static generateUserQuotaBaseSQL (options: { - whereUserId: '$userId' | '"UserModel"."id"' - withSelect: boolean - where?: string - }) { - const andWhere = options.where - ? 'AND ' + options.where - : '' - - const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + - 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + - `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` - - const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + - 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + - videoChannelJoin - - const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + - 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' + - 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' + - videoChannelJoin - - return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + - 'FROM (' + - `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` + - 'GROUP BY "t1"."videoId"' + - ') t2' - } - - static getTotalRawQuery (query: string, userId: number) { - const options = { - bind: { userId }, - type: QueryTypes.SELECT as QueryTypes.SELECT - } - - return UserModel.sequelize.query<{ total: string }>(query, options) - .then(([ { total } ]) => { - if (total === null) return 0 - - return parseInt(total, 10) - }) - } - - static async getStats () { - function getActiveUsers (days: number) { - const query = { - where: { - [Op.and]: [ - literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`) - ] - } - } - - return UserModel.count(query) - } - - const totalUsers = await UserModel.count() - const totalDailyActiveUsers = await getActiveUsers(1) - const totalWeeklyActiveUsers = await getActiveUsers(7) - const totalMonthlyActiveUsers = await getActiveUsers(30) - const totalHalfYearActiveUsers = await getActiveUsers(180) - - return { - totalUsers, - totalDailyActiveUsers, - totalWeeklyActiveUsers, - totalMonthlyActiveUsers, - totalHalfYearActiveUsers - } - } - - static autoComplete (search: string) { - const query = { - where: { - username: { - [Op.like]: `%${search}%` - } - }, - limit: 10 - } - - return UserModel.findAll(query) - .then(u => u.map(u => u.username)) - } - - canGetVideo (video: MVideoWithRights) { - const videoUserId = video.VideoChannel.Account.userId - - if (video.isBlacklisted()) { - return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) - } - - if (video.privacy === VideoPrivacy.PRIVATE) { - return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) - } - - if (video.privacy === VideoPrivacy.INTERNAL) return true - - return false - } - - hasRight (right: UserRight) { - return hasUserRight(this.role, right) - } - - hasAdminFlag (flag: UserAdminFlag) { - return this.adminFlags & flag - } - - isPasswordMatch (password: string) { - return comparePassword(password, this.password) - } - - toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { - const videoQuotaUsed = this.get('videoQuotaUsed') - const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') - const videosCount = this.get('videosCount') - const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') - const abusesCreatedCount = this.get('abusesCreatedCount') - const videoCommentsCount = this.get('videoCommentsCount') - - const json: User = { - id: this.id, - username: this.username, - email: this.email, - theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME), - - pendingEmail: this.pendingEmail, - emailVerified: this.emailVerified, - - nsfwPolicy: this.nsfwPolicy, - webTorrentEnabled: this.webTorrentEnabled, - videosHistoryEnabled: this.videosHistoryEnabled, - autoPlayVideo: this.autoPlayVideo, - autoPlayNextVideo: this.autoPlayNextVideo, - autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, - videoLanguages: this.videoLanguages, - - role: this.role, - roleLabel: USER_ROLE_LABELS[this.role], - - videoQuota: this.videoQuota, - videoQuotaDaily: this.videoQuotaDaily, - videoQuotaUsed: videoQuotaUsed !== undefined - ? parseInt(videoQuotaUsed + '', 10) - : undefined, - videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined - ? parseInt(videoQuotaUsedDaily + '', 10) - : undefined, - videosCount: videosCount !== undefined - ? parseInt(videosCount + '', 10) - : undefined, - abusesCount: abusesCount - ? parseInt(abusesCount, 10) - : undefined, - abusesAcceptedCount: abusesAcceptedCount - ? parseInt(abusesAcceptedCount, 10) - : undefined, - abusesCreatedCount: abusesCreatedCount !== undefined - ? parseInt(abusesCreatedCount + '', 10) - : undefined, - videoCommentsCount: videoCommentsCount !== undefined - ? parseInt(videoCommentsCount + '', 10) - : undefined, - - noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, - noWelcomeModal: this.noWelcomeModal, - - blocked: this.blocked, - blockedReason: this.blockedReason, - - account: this.Account.toFormattedJSON(), - - notificationSettings: this.NotificationSetting - ? this.NotificationSetting.toFormattedJSON() - : undefined, - - videoChannels: [], - - createdAt: this.createdAt, - - pluginAuth: this.pluginAuth, - - lastLoginDate: this.lastLoginDate - } - - if (parameters.withAdminFlags) { - Object.assign(json, { adminFlags: this.adminFlags }) - } - - if (Array.isArray(this.Account.VideoChannels) === true) { - json.videoChannels = this.Account.VideoChannels - .map(c => c.toFormattedJSON()) - .sort((v1, v2) => { - if (v1.createdAt < v2.createdAt) return -1 - if (v1.createdAt === v2.createdAt) return 0 - - return 1 - }) - } - - return json - } - - toMeFormattedJSON (this: MMyUserFormattable): MyUser { - const formatted = this.toFormattedJSON() - - const specialPlaylists = this.Account.VideoPlaylists - .map(p => ({ id: p.id, name: p.name, type: p.type })) - - return Object.assign(formatted, { specialPlaylists }) - } -} diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts deleted file mode 100644 index 4c5f37620..000000000 --- a/server/models/activitypub/actor-follow.ts +++ /dev/null @@ -1,721 +0,0 @@ -import { difference, values } from 'lodash' -import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' -import { - AfterCreate, - AfterDestroy, - AfterUpdate, - AllowNull, - BelongsTo, - Column, - CreatedAt, - DataType, - Default, - ForeignKey, - Is, - IsInt, - Max, - Model, - Table, - UpdatedAt -} from 'sequelize-typescript' -import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' -import { getServerActor } from '@server/models/application/application' -import { VideoModel } from '@server/models/video/video' -import { - MActorFollowActorsDefault, - MActorFollowActorsDefaultSubscription, - MActorFollowFollowingHost, - MActorFollowFormattable, - MActorFollowSubscriptions -} from '@server/types/models' -import { ActivityPubActorType } from '@shared/models' -import { FollowState } from '../../../shared/models/actors' -import { ActorFollow } from '../../../shared/models/actors/follow.model' -import { logger } from '../../helpers/logger' -import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' -import { AccountModel } from '../account/account' -import { ServerModel } from '../server/server' -import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' -import { VideoChannelModel } from '../video/video-channel' -import { ActorModel, unusedActorAttributesForAPI } from './actor' - -@Table({ - tableName: 'actorFollow', - indexes: [ - { - fields: [ 'actorId' ] - }, - { - fields: [ 'targetActorId' ] - }, - { - fields: [ 'actorId', 'targetActorId' ], - unique: true - }, - { - fields: [ 'score' ] - }, - { - fields: [ 'url' ], - unique: true - } - ] -}) -export class ActorFollowModel extends Model { - - @AllowNull(false) - @Column(DataType.ENUM(...values(FOLLOW_STATES))) - state: FollowState - - @AllowNull(false) - @Default(ACTOR_FOLLOW_SCORE.BASE) - @IsInt - @Max(ACTOR_FOLLOW_SCORE.MAX) - @Column - score: number - - // Allow null because we added this column in PeerTube v3, and don't want to generate fake URLs of remote follows - @AllowNull(true) - @Is('ActorFollowUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) - url: string - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @ForeignKey(() => ActorModel) - @Column - actorId: number - - @BelongsTo(() => ActorModel, { - foreignKey: { - name: 'actorId', - allowNull: false - }, - as: 'ActorFollower', - onDelete: 'CASCADE' - }) - ActorFollower: ActorModel - - @ForeignKey(() => ActorModel) - @Column - targetActorId: number - - @BelongsTo(() => ActorModel, { - foreignKey: { - name: 'targetActorId', - allowNull: false - }, - as: 'ActorFollowing', - onDelete: 'CASCADE' - }) - ActorFollowing: ActorModel - - @AfterCreate - @AfterUpdate - static incrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { - if (instance.state !== 'accepted') return undefined - - return Promise.all([ - ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), - ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) - ]) - } - - @AfterDestroy - static decrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { - return Promise.all([ - ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), - ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) - ]) - } - - static removeFollowsOf (actorId: number, t?: Transaction) { - const query = { - where: { - [Op.or]: [ - { - actorId - }, - { - targetActorId: actorId - } - ] - }, - transaction: t - } - - return ActorFollowModel.destroy(query) - } - - // Remove actor follows with a score of 0 (too many requests where they were unreachable) - static async removeBadActorFollows () { - const actorFollows = await ActorFollowModel.listBadActorFollows() - - const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy()) - await Promise.all(actorFollowsRemovePromises) - - const numberOfActorFollowsRemoved = actorFollows.length - - if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) - } - - static isFollowedBy (actorId: number, followerActorId: number) { - const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' - const options = { - type: QueryTypes.SELECT as QueryTypes.SELECT, - bind: { actorId, followerActorId }, - raw: true - } - - return VideoModel.sequelize.query(query, options) - .then(results => results.length === 1) - } - - static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise { - const query = { - where: { - actorId, - targetActorId: targetActorId - }, - include: [ - { - model: ActorModel, - required: true, - as: 'ActorFollower' - }, - { - model: ActorModel, - required: true, - as: 'ActorFollowing' - } - ], - transaction: t - } - - return ActorFollowModel.findOne(query) - } - - static loadByActorAndTargetNameAndHostForAPI ( - actorId: number, - targetName: string, - targetHost: string, - t?: Transaction - ): Promise { - const actorFollowingPartInclude: IncludeOptions = { - model: ActorModel, - required: true, - as: 'ActorFollowing', - where: { - preferredUsername: targetName - }, - include: [ - { - model: VideoChannelModel.unscoped(), - required: false - } - ] - } - - if (targetHost === null) { - actorFollowingPartInclude.where['serverId'] = null - } else { - actorFollowingPartInclude.include.push({ - model: ServerModel, - required: true, - where: { - host: targetHost - } - }) - } - - const query = { - where: { - actorId - }, - include: [ - actorFollowingPartInclude, - { - model: ActorModel, - required: true, - as: 'ActorFollower' - } - ], - transaction: t - } - - return ActorFollowModel.findOne(query) - } - - static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise { - const whereTab = targets - .map(t => { - if (t.host) { - return { - [Op.and]: [ - { - $preferredUsername$: t.name - }, - { - $host$: t.host - } - ] - } - } - - return { - [Op.and]: [ - { - $preferredUsername$: t.name - }, - { - $serverId$: null - } - ] - } - }) - - const query = { - attributes: [ 'id' ], - where: { - [Op.and]: [ - { - [Op.or]: whereTab - }, - { - actorId - } - ] - }, - include: [ - { - attributes: [ 'preferredUsername' ], - model: ActorModel.unscoped(), - required: true, - as: 'ActorFollowing', - include: [ - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - } - ] - } - ] - } - - return ActorFollowModel.findAll(query) - } - - static listFollowingForApi (options: { - id: number - start: number - count: number - sort: string - state?: FollowState - actorType?: ActivityPubActorType - search?: string - }) { - const { id, start, count, sort, search, state, actorType } = options - - const followWhere = state ? { state } : {} - const followingWhere: WhereOptions = {} - const followingServerWhere: WhereOptions = {} - - if (search) { - Object.assign(followingServerWhere, { - host: { - [Op.iLike]: '%' + search + '%' - } - }) - } - - if (actorType) { - Object.assign(followingWhere, { type: actorType }) - } - - const query = { - distinct: true, - offset: start, - limit: count, - order: getFollowsSort(sort), - where: followWhere, - include: [ - { - model: ActorModel, - required: true, - as: 'ActorFollower', - where: { - id - } - }, - { - model: ActorModel, - as: 'ActorFollowing', - required: true, - where: followingWhere, - include: [ - { - model: ServerModel, - required: true, - where: followingServerWhere - } - ] - } - ] - } - - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) - } - - static listFollowersForApi (options: { - actorId: number - start: number - count: number - sort: string - state?: FollowState - actorType?: ActivityPubActorType - search?: string - }) { - const { actorId, start, count, sort, search, state, actorType } = options - - const followWhere = state ? { state } : {} - const followerWhere: WhereOptions = {} - const followerServerWhere: WhereOptions = {} - - if (search) { - Object.assign(followerServerWhere, { - host: { - [Op.iLike]: '%' + search + '%' - } - }) - } - - if (actorType) { - Object.assign(followerWhere, { type: actorType }) - } - - const query = { - distinct: true, - offset: start, - limit: count, - order: getFollowsSort(sort), - where: followWhere, - include: [ - { - model: ActorModel, - required: true, - as: 'ActorFollower', - where: followerWhere, - include: [ - { - model: ServerModel, - required: true, - where: followerServerWhere - } - ] - }, - { - model: ActorModel, - as: 'ActorFollowing', - required: true, - where: { - id: actorId - } - } - ] - } - - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) - } - - static listSubscriptionsForApi (options: { - actorId: number - start: number - count: number - sort: string - search?: string - }) { - const { actorId, start, count, sort } = options - const where = { - actorId: actorId - } - - if (options.search) { - Object.assign(where, { - [Op.or]: [ - searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), - searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$') - ] - }) - } - - const query = { - attributes: [], - distinct: true, - offset: start, - limit: count, - order: getSort(sort), - where, - include: [ - { - attributes: [ 'id' ], - model: ActorModel.unscoped(), - as: 'ActorFollowing', - required: true, - include: [ - { - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: { - exclude: unusedActorAttributesForAPI - }, - model: ActorModel, - required: true - }, - { - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: { - exclude: unusedActorAttributesForAPI - }, - model: ActorModel, - required: true - } - ] - } - ] - } - ] - } - ] - } - - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows.map(r => r.ActorFollowing.VideoChannel), - total: count - } - }) - } - - static async keepUnfollowedInstance (hosts: string[]) { - const followerId = (await getServerActor()).id - - const query = { - attributes: [ 'id' ], - where: { - actorId: followerId - }, - include: [ - { - attributes: [ 'id' ], - model: ActorModel.unscoped(), - required: true, - as: 'ActorFollowing', - where: { - preferredUsername: SERVER_ACTOR_NAME - }, - include: [ - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: true, - where: { - host: { - [Op.in]: hosts - } - } - } - ] - } - ] - } - - const res = await ActorFollowModel.findAll(query) - const followedHosts = res.map(row => row.ActorFollowing.Server.host) - - return difference(hosts, followedHosts) - } - - static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) { - return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) - } - - static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) { - return ActorFollowModel.createListAcceptedFollowForApiQuery( - 'followers', - actorIds, - t, - undefined, - undefined, - 'sharedInboxUrl', - true - ) - } - - static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) { - return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) - } - - static async getStats () { - const serverActor = await getServerActor() - - const totalInstanceFollowing = await ActorFollowModel.count({ - where: { - actorId: serverActor.id - } - }) - - const totalInstanceFollowers = await ActorFollowModel.count({ - where: { - targetActorId: serverActor.id - } - }) - - return { - totalInstanceFollowing, - totalInstanceFollowers - } - } - - static updateScore (inboxUrl: string, value: number, t?: Transaction) { - const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + - 'WHERE id IN (' + - 'SELECT "actorFollow"."id" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + - `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + - ')' - - const options = { - type: QueryTypes.BULKUPDATE, - transaction: t - } - - return ActorFollowModel.sequelize.query(query, options) - } - - static async updateScoreByFollowingServers (serverIds: number[], value: number, t?: Transaction) { - if (serverIds.length === 0) return - - const me = await getServerActor() - const serverIdsString = createSafeIn(ActorFollowModel, serverIds) - - const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + - 'WHERE id IN (' + - 'SELECT "actorFollow"."id" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' + - `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` + // I'm the follower - `AND "actor"."serverId" IN (${serverIdsString})` + // Criteria on followings - ')' - - const options = { - type: QueryTypes.BULKUPDATE, - transaction: t - } - - return ActorFollowModel.sequelize.query(query, options) - } - - private static async createListAcceptedFollowForApiQuery ( - type: 'followers' | 'following', - actorIds: number[], - t: Transaction, - start?: number, - count?: number, - columnUrl = 'url', - distinct = false - ) { - let firstJoin: string - let secondJoin: string - - if (type === 'followers') { - firstJoin = 'targetActorId' - secondJoin = 'actorId' - } else { - firstJoin = 'actorId' - secondJoin = 'targetActorId' - } - - const selections: string[] = [] - if (distinct === true) selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`) - else selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`) - - selections.push('COUNT(*) AS "total"') - - const tasks: Promise[] = [] - - for (const selection of selections) { - let query = 'SELECT ' + selection + ' FROM "actor" ' + - 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + - 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + - `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL ` - - if (count !== undefined) query += 'LIMIT ' + count - if (start !== undefined) query += ' OFFSET ' + start - - const options = { - bind: { actorIds }, - type: QueryTypes.SELECT, - transaction: t - } - tasks.push(ActorFollowModel.sequelize.query(query, options)) - } - - const [ followers, [ dataTotal ] ] = await Promise.all(tasks) - const urls: string[] = followers.map(f => f.selectionUrl) - - return { - data: urls, - total: dataTotal ? parseInt(dataTotal.total, 10) : 0 - } - } - - private static listBadActorFollows () { - const query = { - where: { - score: { - [Op.lte]: 0 - } - }, - logging: false - } - - return ActorFollowModel.findAll(query) - } - - toFormattedJSON (this: MActorFollowFormattable): ActorFollow { - const follower = this.ActorFollower.toFormattedJSON() - const following = this.ActorFollowing.toFormattedJSON() - - return { - id: this.id, - follower, - following, - score: this.score, - state: this.state, - createdAt: this.createdAt, - updatedAt: this.updatedAt - } - } -} diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts deleted file mode 100644 index 1af9efac2..000000000 --- a/server/models/activitypub/actor.ts +++ /dev/null @@ -1,699 +0,0 @@ -import { values } from 'lodash' -import { extname } from 'path' -import { literal, Op, Transaction } from 'sequelize' -import { - AllowNull, - BelongsTo, - Column, - CreatedAt, - DataType, - DefaultScope, - ForeignKey, - HasMany, - HasOne, - Is, - Model, - Scopes, - Table, - UpdatedAt -} from 'sequelize-typescript' -import { ModelCache } from '@server/models/model-cache' -import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' -import { ActorImage } from '../../../shared/models/actors/actor-image.model' -import { activityPubContextify } from '../../helpers/activitypub' -import { - isActorFollowersCountValid, - isActorFollowingCountValid, - isActorPreferredUsernameValid, - isActorPrivateKeyValid, - isActorPublicKeyValid -} from '../../helpers/custom-validators/activitypub/actor' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { - ACTIVITY_PUB, - ACTIVITY_PUB_ACTOR_TYPES, - CONSTRAINTS_FIELDS, - MIMETYPES, - SERVER_ACTOR_NAME, - WEBSERVER -} from '../../initializers/constants' -import { - MActor, - MActorAccountChannelId, - MActorAPAccount, - MActorAPChannel, - MActorFormattable, - MActorFull, - MActorHost, - MActorServer, - MActorSummaryFormattable, - MActorUrl, - MActorWithInboxes -} from '../../types/models' -import { AccountModel } from '../account/account' -import { ActorImageModel } from '../account/actor-image' -import { ServerModel } from '../server/server' -import { isOutdated, throwIfNotValid } from '../utils' -import { VideoModel } from '../video/video' -import { VideoChannelModel } from '../video/video-channel' -import { ActorFollowModel } from './actor-follow' - -enum ScopeNames { - FULL = 'FULL' -} - -export const unusedActorAttributesForAPI = [ - 'publicKey', - 'privateKey', - 'inboxUrl', - 'outboxUrl', - 'sharedInboxUrl', - 'followersUrl', - 'followingUrl' -] - -@DefaultScope(() => ({ - include: [ - { - model: ServerModel, - required: false - }, - { - model: ActorImageModel, - as: 'Avatar', - required: false - } - ] -})) -@Scopes(() => ({ - [ScopeNames.FULL]: { - include: [ - { - model: AccountModel.unscoped(), - required: false - }, - { - model: VideoChannelModel.unscoped(), - required: false, - include: [ - { - model: AccountModel, - required: true - } - ] - }, - { - model: ServerModel, - required: false - }, - { - model: ActorImageModel, - as: 'Avatar', - required: false - }, - { - model: ActorImageModel, - as: 'Banner', - required: false - } - ] - } -})) -@Table({ - tableName: 'actor', - indexes: [ - { - fields: [ 'url' ], - unique: true - }, - { - fields: [ 'preferredUsername', 'serverId' ], - unique: true, - where: { - serverId: { - [Op.ne]: null - } - } - }, - { - fields: [ 'preferredUsername' ], - unique: true, - where: { - serverId: null - } - }, - { - fields: [ 'inboxUrl', 'sharedInboxUrl' ] - }, - { - fields: [ 'sharedInboxUrl' ] - }, - { - fields: [ 'serverId' ] - }, - { - fields: [ 'avatarId' ] - }, - { - fields: [ 'followersUrl' ] - } - ] -}) -export class ActorModel extends Model { - - @AllowNull(false) - @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) - type: ActivityPubActorType - - @AllowNull(false) - @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username')) - @Column - preferredUsername: string - - @AllowNull(false) - @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - url: string - - @AllowNull(true) - @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max)) - publicKey: string - - @AllowNull(true) - @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max)) - privateKey: string - - @AllowNull(false) - @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count')) - @Column - followersCount: number - - @AllowNull(false) - @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count')) - @Column - followingCount: number - - @AllowNull(false) - @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - inboxUrl: string - - @AllowNull(true) - @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - outboxUrl: string - - @AllowNull(true) - @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - sharedInboxUrl: string - - @AllowNull(true) - @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - followersUrl: string - - @AllowNull(true) - @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true)) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) - followingUrl: string - - @AllowNull(true) - @Column - remoteCreatedAt: Date - - @CreatedAt - createdAt: Date - - @UpdatedAt - updatedAt: Date - - @ForeignKey(() => ActorImageModel) - @Column - avatarId: number - - @ForeignKey(() => ActorImageModel) - @Column - bannerId: number - - @BelongsTo(() => ActorImageModel, { - foreignKey: { - name: 'avatarId', - allowNull: true - }, - as: 'Avatar', - onDelete: 'set null', - hooks: true - }) - Avatar: ActorImageModel - - @BelongsTo(() => ActorImageModel, { - foreignKey: { - name: 'bannerId', - allowNull: true - }, - as: 'Banner', - onDelete: 'set null', - hooks: true - }) - Banner: ActorImageModel - - @HasMany(() => ActorFollowModel, { - foreignKey: { - name: 'actorId', - allowNull: false - }, - as: 'ActorFollowings', - onDelete: 'cascade' - }) - ActorFollowing: ActorFollowModel[] - - @HasMany(() => ActorFollowModel, { - foreignKey: { - name: 'targetActorId', - allowNull: false - }, - as: 'ActorFollowers', - onDelete: 'cascade' - }) - ActorFollowers: ActorFollowModel[] - - @ForeignKey(() => ServerModel) - @Column - serverId: number - - @BelongsTo(() => ServerModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Server: ServerModel - - @HasOne(() => AccountModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade', - hooks: true - }) - Account: AccountModel - - @HasOne(() => VideoChannelModel, { - foreignKey: { - allowNull: true - }, - onDelete: 'cascade', - hooks: true - }) - VideoChannel: VideoChannelModel - - static load (id: number): Promise { - return ActorModel.unscoped().findByPk(id) - } - - static loadFull (id: number): Promise { - return ActorModel.scope(ScopeNames.FULL).findByPk(id) - } - - static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise { - const query = { - include: [ - { - attributes: [ 'id' ], - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id' ], - model: VideoModel.unscoped(), - required: true, - where: { - id: videoId - } - } - ] - } - ] - } - ], - transaction - } - - return ActorModel.unscoped().findOne(query) - } - - static isActorUrlExist (url: string) { - const query = { - raw: true, - where: { - url - } - } - - return ActorModel.unscoped().findOne(query) - .then(a => !!a) - } - - static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise { - const query = { - where: { - followersUrl: { - [Op.in]: followersUrls - } - }, - transaction - } - - return ActorModel.scope(ScopeNames.FULL).findAll(query) - } - - static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise { - const fun = () => { - const query = { - where: { - preferredUsername, - serverId: null - }, - transaction - } - - return ActorModel.scope(ScopeNames.FULL) - .findOne(query) - } - - return ModelCache.Instance.doCache({ - cacheType: 'local-actor-name', - key: preferredUsername, - // The server actor never change, so we can easily cache it - whitelist: () => preferredUsername === SERVER_ACTOR_NAME, - fun - }) - } - - static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise { - const fun = () => { - const query = { - attributes: [ 'url' ], - where: { - preferredUsername, - serverId: null - }, - transaction - } - - return ActorModel.unscoped() - .findOne(query) - } - - return ModelCache.Instance.doCache({ - cacheType: 'local-actor-name', - key: preferredUsername, - // The server actor never change, so we can easily cache it - whitelist: () => preferredUsername === SERVER_ACTOR_NAME, - fun - }) - } - - static loadByNameAndHost (preferredUsername: string, host: string): Promise { - const query = { - where: { - preferredUsername - }, - include: [ - { - model: ServerModel, - required: true, - where: { - host - } - } - ] - } - - return ActorModel.scope(ScopeNames.FULL).findOne(query) - } - - static loadByUrl (url: string, transaction?: Transaction): Promise { - const query = { - where: { - url - }, - transaction, - include: [ - { - attributes: [ 'id' ], - model: AccountModel.unscoped(), - required: false - }, - { - attributes: [ 'id' ], - model: VideoChannelModel.unscoped(), - required: false - } - ] - } - - return ActorModel.unscoped().findOne(query) - } - - static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise { - const query = { - where: { - url - }, - transaction - } - - return ActorModel.scope(ScopeNames.FULL).findOne(query) - } - - static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) { - const sanitizedOfId = parseInt(ofId + '', 10) - const where = { id: sanitizedOfId } - - let columnToUpdate: string - let columnOfCount: string - - if (type === 'followers') { - columnToUpdate = 'followersCount' - columnOfCount = 'targetActorId' - } else { - columnToUpdate = 'followingCount' - columnOfCount = 'actorId' - } - - return ActorModel.update({ - [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`) - }, { where, transaction }) - } - - static loadAccountActorByVideoId (videoId: number): Promise { - const query = { - include: [ - { - attributes: [ 'id' ], - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id', 'accountId' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: [ 'id', 'channelId' ], - model: VideoModel.unscoped(), - where: { - id: videoId - } - } - ] - } - ] - } - ] - } - - return ActorModel.unscoped().findOne(query) - } - - getSharedInbox (this: MActorWithInboxes) { - return this.sharedInboxUrl || this.inboxUrl - } - - toFormattedSummaryJSON (this: MActorSummaryFormattable) { - let avatar: ActorImage = null - if (this.Avatar) { - avatar = this.Avatar.toFormattedJSON() - } - - return { - url: this.url, - name: this.preferredUsername, - host: this.getHost(), - avatar - } - } - - toFormattedJSON (this: MActorFormattable) { - const base = this.toFormattedSummaryJSON() - - let banner: ActorImage = null - if (this.Banner) { - banner = this.Banner.toFormattedJSON() - } - - return Object.assign(base, { - id: this.id, - hostRedundancyAllowed: this.getRedundancyAllowed(), - followingCount: this.followingCount, - followersCount: this.followersCount, - banner, - createdAt: this.getCreatedAt() - }) - } - - toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { - let icon: ActivityIconObject - let image: ActivityIconObject - - if (this.avatarId) { - const extension = extname(this.Avatar.filename) - - icon = { - type: 'Image', - mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], - height: this.Avatar.height, - width: this.Avatar.width, - url: this.getAvatarUrl() - } - } - - if (this.bannerId) { - const banner = (this as MActorAPChannel).Banner - const extension = extname(banner.filename) - - image = { - type: 'Image', - mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], - height: banner.height, - width: banner.width, - url: this.getBannerUrl() - } - } - - const json = { - type: this.type, - id: this.url, - following: this.getFollowingUrl(), - followers: this.getFollowersUrl(), - playlists: this.getPlaylistsUrl(), - inbox: this.inboxUrl, - outbox: this.outboxUrl, - preferredUsername: this.preferredUsername, - url: this.url, - name, - endpoints: { - sharedInbox: this.sharedInboxUrl - }, - publicKey: { - id: this.getPublicKeyUrl(), - owner: this.url, - publicKeyPem: this.publicKey - }, - published: this.getCreatedAt().toISOString(), - icon, - image - } - - return activityPubContextify(json) - } - - getFollowerSharedInboxUrls (t: Transaction) { - const query = { - attributes: [ 'sharedInboxUrl' ], - include: [ - { - attribute: [], - model: ActorFollowModel.unscoped(), - required: true, - as: 'ActorFollowing', - where: { - state: 'accepted', - targetActorId: this.id - } - } - ], - transaction: t - } - - return ActorModel.findAll(query) - .then(accounts => accounts.map(a => a.sharedInboxUrl)) - } - - getFollowingUrl () { - return this.url + '/following' - } - - getFollowersUrl () { - return this.url + '/followers' - } - - getPlaylistsUrl () { - return this.url + '/playlists' - } - - getPublicKeyUrl () { - return this.url + '#main-key' - } - - isOwned () { - return this.serverId === null - } - - getWebfingerUrl (this: MActorServer) { - return 'acct:' + this.preferredUsername + '@' + this.getHost() - } - - getIdentifier () { - return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername - } - - getHost (this: MActorHost) { - return this.Server ? this.Server.host : WEBSERVER.HOST - } - - getRedundancyAllowed () { - return this.Server ? this.Server.redundancyAllowed : false - } - - getAvatarUrl () { - if (!this.avatarId) return undefined - - return WEBSERVER.URL + this.Avatar.getStaticPath() - } - - getBannerUrl () { - if (!this.bannerId) return undefined - - return WEBSERVER.URL + this.Banner.getStaticPath() - } - - isOutdated () { - if (this.isOwned()) return false - - return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL) - } - - getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) { - return this.remoteCreatedAt || this.createdAt - } -} diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts new file mode 100644 index 000000000..4c5f37620 --- /dev/null +++ b/server/models/actor/actor-follow.ts @@ -0,0 +1,721 @@ +import { difference, values } from 'lodash' +import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' +import { + AfterCreate, + AfterDestroy, + AfterUpdate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + Is, + IsInt, + Max, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' +import { getServerActor } from '@server/models/application/application' +import { VideoModel } from '@server/models/video/video' +import { + MActorFollowActorsDefault, + MActorFollowActorsDefaultSubscription, + MActorFollowFollowingHost, + MActorFollowFormattable, + MActorFollowSubscriptions +} from '@server/types/models' +import { ActivityPubActorType } from '@shared/models' +import { FollowState } from '../../../shared/models/actors' +import { ActorFollow } from '../../../shared/models/actors/follow.model' +import { logger } from '../../helpers/logger' +import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' +import { AccountModel } from '../account/account' +import { ServerModel } from '../server/server' +import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' +import { VideoChannelModel } from '../video/video-channel' +import { ActorModel, unusedActorAttributesForAPI } from './actor' + +@Table({ + tableName: 'actorFollow', + indexes: [ + { + fields: [ 'actorId' ] + }, + { + fields: [ 'targetActorId' ] + }, + { + fields: [ 'actorId', 'targetActorId' ], + unique: true + }, + { + fields: [ 'score' ] + }, + { + fields: [ 'url' ], + unique: true + } + ] +}) +export class ActorFollowModel extends Model { + + @AllowNull(false) + @Column(DataType.ENUM(...values(FOLLOW_STATES))) + state: FollowState + + @AllowNull(false) + @Default(ACTOR_FOLLOW_SCORE.BASE) + @IsInt + @Max(ACTOR_FOLLOW_SCORE.MAX) + @Column + score: number + + // Allow null because we added this column in PeerTube v3, and don't want to generate fake URLs of remote follows + @AllowNull(true) + @Is('ActorFollowUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) + url: string + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => ActorModel) + @Column + actorId: number + + @BelongsTo(() => ActorModel, { + foreignKey: { + name: 'actorId', + allowNull: false + }, + as: 'ActorFollower', + onDelete: 'CASCADE' + }) + ActorFollower: ActorModel + + @ForeignKey(() => ActorModel) + @Column + targetActorId: number + + @BelongsTo(() => ActorModel, { + foreignKey: { + name: 'targetActorId', + allowNull: false + }, + as: 'ActorFollowing', + onDelete: 'CASCADE' + }) + ActorFollowing: ActorModel + + @AfterCreate + @AfterUpdate + static incrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { + if (instance.state !== 'accepted') return undefined + + return Promise.all([ + ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), + ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) + ]) + } + + @AfterDestroy + static decrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { + return Promise.all([ + ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), + ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) + ]) + } + + static removeFollowsOf (actorId: number, t?: Transaction) { + const query = { + where: { + [Op.or]: [ + { + actorId + }, + { + targetActorId: actorId + } + ] + }, + transaction: t + } + + return ActorFollowModel.destroy(query) + } + + // Remove actor follows with a score of 0 (too many requests where they were unreachable) + static async removeBadActorFollows () { + const actorFollows = await ActorFollowModel.listBadActorFollows() + + const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy()) + await Promise.all(actorFollowsRemovePromises) + + const numberOfActorFollowsRemoved = actorFollows.length + + if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) + } + + static isFollowedBy (actorId: number, followerActorId: number) { + const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' + const options = { + type: QueryTypes.SELECT as QueryTypes.SELECT, + bind: { actorId, followerActorId }, + raw: true + } + + return VideoModel.sequelize.query(query, options) + .then(results => results.length === 1) + } + + static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise { + const query = { + where: { + actorId, + targetActorId: targetActorId + }, + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower' + }, + { + model: ActorModel, + required: true, + as: 'ActorFollowing' + } + ], + transaction: t + } + + return ActorFollowModel.findOne(query) + } + + static loadByActorAndTargetNameAndHostForAPI ( + actorId: number, + targetName: string, + targetHost: string, + t?: Transaction + ): Promise { + const actorFollowingPartInclude: IncludeOptions = { + model: ActorModel, + required: true, + as: 'ActorFollowing', + where: { + preferredUsername: targetName + }, + include: [ + { + model: VideoChannelModel.unscoped(), + required: false + } + ] + } + + if (targetHost === null) { + actorFollowingPartInclude.where['serverId'] = null + } else { + actorFollowingPartInclude.include.push({ + model: ServerModel, + required: true, + where: { + host: targetHost + } + }) + } + + const query = { + where: { + actorId + }, + include: [ + actorFollowingPartInclude, + { + model: ActorModel, + required: true, + as: 'ActorFollower' + } + ], + transaction: t + } + + return ActorFollowModel.findOne(query) + } + + static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise { + const whereTab = targets + .map(t => { + if (t.host) { + return { + [Op.and]: [ + { + $preferredUsername$: t.name + }, + { + $host$: t.host + } + ] + } + } + + return { + [Op.and]: [ + { + $preferredUsername$: t.name + }, + { + $serverId$: null + } + ] + } + }) + + const query = { + attributes: [ 'id' ], + where: { + [Op.and]: [ + { + [Op.or]: whereTab + }, + { + actorId + } + ] + }, + include: [ + { + attributes: [ 'preferredUsername' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + include: [ + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + } + ] + } + + return ActorFollowModel.findAll(query) + } + + static listFollowingForApi (options: { + id: number + start: number + count: number + sort: string + state?: FollowState + actorType?: ActivityPubActorType + search?: string + }) { + const { id, start, count, sort, search, state, actorType } = options + + const followWhere = state ? { state } : {} + const followingWhere: WhereOptions = {} + const followingServerWhere: WhereOptions = {} + + if (search) { + Object.assign(followingServerWhere, { + host: { + [Op.iLike]: '%' + search + '%' + } + }) + } + + if (actorType) { + Object.assign(followingWhere, { type: actorType }) + } + + const query = { + distinct: true, + offset: start, + limit: count, + order: getFollowsSort(sort), + where: followWhere, + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower', + where: { + id + } + }, + { + model: ActorModel, + as: 'ActorFollowing', + required: true, + where: followingWhere, + include: [ + { + model: ServerModel, + required: true, + where: followingServerWhere + } + ] + } + ] + } + + return ActorFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listFollowersForApi (options: { + actorId: number + start: number + count: number + sort: string + state?: FollowState + actorType?: ActivityPubActorType + search?: string + }) { + const { actorId, start, count, sort, search, state, actorType } = options + + const followWhere = state ? { state } : {} + const followerWhere: WhereOptions = {} + const followerServerWhere: WhereOptions = {} + + if (search) { + Object.assign(followerServerWhere, { + host: { + [Op.iLike]: '%' + search + '%' + } + }) + } + + if (actorType) { + Object.assign(followerWhere, { type: actorType }) + } + + const query = { + distinct: true, + offset: start, + limit: count, + order: getFollowsSort(sort), + where: followWhere, + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower', + where: followerWhere, + include: [ + { + model: ServerModel, + required: true, + where: followerServerWhere + } + ] + }, + { + model: ActorModel, + as: 'ActorFollowing', + required: true, + where: { + id: actorId + } + } + ] + } + + return ActorFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listSubscriptionsForApi (options: { + actorId: number + start: number + count: number + sort: string + search?: string + }) { + const { actorId, start, count, sort } = options + const where = { + actorId: actorId + } + + if (options.search) { + Object.assign(where, { + [Op.or]: [ + searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), + searchAttribute(options.search, '$ActorFollowing.VideoChannel.name$') + ] + }) + } + + const query = { + attributes: [], + distinct: true, + offset: start, + limit: count, + order: getSort(sort), + where, + include: [ + { + attributes: [ 'id' ], + model: ActorModel.unscoped(), + as: 'ActorFollowing', + required: true, + include: [ + { + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: { + exclude: unusedActorAttributesForAPI + }, + model: ActorModel, + required: true + }, + { + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: { + exclude: unusedActorAttributesForAPI + }, + model: ActorModel, + required: true + } + ] + } + ] + } + ] + } + ] + } + + return ActorFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows.map(r => r.ActorFollowing.VideoChannel), + total: count + } + }) + } + + static async keepUnfollowedInstance (hosts: string[]) { + const followerId = (await getServerActor()).id + + const query = { + attributes: [ 'id' ], + where: { + actorId: followerId + }, + include: [ + { + attributes: [ 'id' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + where: { + preferredUsername: SERVER_ACTOR_NAME + }, + include: [ + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: true, + where: { + host: { + [Op.in]: hosts + } + } + } + ] + } + ] + } + + const res = await ActorFollowModel.findAll(query) + const followedHosts = res.map(row => row.ActorFollowing.Server.host) + + return difference(hosts, followedHosts) + } + + static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) { + return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) + } + + static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) { + return ActorFollowModel.createListAcceptedFollowForApiQuery( + 'followers', + actorIds, + t, + undefined, + undefined, + 'sharedInboxUrl', + true + ) + } + + static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) { + return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) + } + + static async getStats () { + const serverActor = await getServerActor() + + const totalInstanceFollowing = await ActorFollowModel.count({ + where: { + actorId: serverActor.id + } + }) + + const totalInstanceFollowers = await ActorFollowModel.count({ + where: { + targetActorId: serverActor.id + } + }) + + return { + totalInstanceFollowing, + totalInstanceFollowers + } + } + + static updateScore (inboxUrl: string, value: number, t?: Transaction) { + const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + + 'WHERE id IN (' + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + + `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + + ')' + + const options = { + type: QueryTypes.BULKUPDATE, + transaction: t + } + + return ActorFollowModel.sequelize.query(query, options) + } + + static async updateScoreByFollowingServers (serverIds: number[], value: number, t?: Transaction) { + if (serverIds.length === 0) return + + const me = await getServerActor() + const serverIdsString = createSafeIn(ActorFollowModel, serverIds) + + const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + + 'WHERE id IN (' + + 'SELECT "actorFollow"."id" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."targetActorId" ' + + `WHERE "actorFollow"."actorId" = ${me.Account.actorId} ` + // I'm the follower + `AND "actor"."serverId" IN (${serverIdsString})` + // Criteria on followings + ')' + + const options = { + type: QueryTypes.BULKUPDATE, + transaction: t + } + + return ActorFollowModel.sequelize.query(query, options) + } + + private static async createListAcceptedFollowForApiQuery ( + type: 'followers' | 'following', + actorIds: number[], + t: Transaction, + start?: number, + count?: number, + columnUrl = 'url', + distinct = false + ) { + let firstJoin: string + let secondJoin: string + + if (type === 'followers') { + firstJoin = 'targetActorId' + secondJoin = 'actorId' + } else { + firstJoin = 'actorId' + secondJoin = 'targetActorId' + } + + const selections: string[] = [] + if (distinct === true) selections.push(`DISTINCT("Follows"."${columnUrl}") AS "selectionUrl"`) + else selections.push(`"Follows"."${columnUrl}" AS "selectionUrl"`) + + selections.push('COUNT(*) AS "total"') + + const tasks: Promise[] = [] + + for (const selection of selections) { + let query = 'SELECT ' + selection + ' FROM "actor" ' + + 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + + 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + + `WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = 'accepted' AND "Follows"."${columnUrl}" IS NOT NULL ` + + if (count !== undefined) query += 'LIMIT ' + count + if (start !== undefined) query += ' OFFSET ' + start + + const options = { + bind: { actorIds }, + type: QueryTypes.SELECT, + transaction: t + } + tasks.push(ActorFollowModel.sequelize.query(query, options)) + } + + const [ followers, [ dataTotal ] ] = await Promise.all(tasks) + const urls: string[] = followers.map(f => f.selectionUrl) + + return { + data: urls, + total: dataTotal ? parseInt(dataTotal.total, 10) : 0 + } + } + + private static listBadActorFollows () { + const query = { + where: { + score: { + [Op.lte]: 0 + } + }, + logging: false + } + + return ActorFollowModel.findAll(query) + } + + toFormattedJSON (this: MActorFollowFormattable): ActorFollow { + const follower = this.ActorFollower.toFormattedJSON() + const following = this.ActorFollowing.toFormattedJSON() + + return { + id: this.id, + follower, + following, + score: this.score, + state: this.state, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } +} diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts new file mode 100644 index 000000000..ae05b4969 --- /dev/null +++ b/server/models/actor/actor-image.ts @@ -0,0 +1,100 @@ +import { remove } from 'fs-extra' +import { join } from 'path' +import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { MActorImageFormattable } from '@server/types/models' +import { ActorImageType } from '@shared/models' +import { ActorImage } from '../../../shared/models/actors/actor-image.model' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' +import { logger } from '../../helpers/logger' +import { CONFIG } from '../../initializers/config' +import { LAZY_STATIC_PATHS } from '../../initializers/constants' +import { throwIfNotValid } from '../utils' + +@Table({ + tableName: 'actorImage', + indexes: [ + { + fields: [ 'filename' ], + unique: true + } + ] +}) +export class ActorImageModel extends Model { + + @AllowNull(false) + @Column + filename: string + + @AllowNull(true) + @Default(null) + @Column + height: number + + @AllowNull(true) + @Default(null) + @Column + width: number + + @AllowNull(true) + @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) + @Column + fileUrl: string + + @AllowNull(false) + @Column + onDisk: boolean + + @AllowNull(false) + @Column + type: ActorImageType + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AfterDestroy + static removeFilesAndSendDelete (instance: ActorImageModel) { + logger.info('Removing actor image file %s.', instance.filename) + + // Don't block the transaction + instance.removeImage() + .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) + } + + static loadByName (filename: string) { + const query = { + where: { + filename + } + } + + return ActorImageModel.findOne(query) + } + + toFormattedJSON (this: MActorImageFormattable): ActorImage { + return { + path: this.getStaticPath(), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + + getStaticPath () { + if (this.type === ActorImageType.AVATAR) { + return join(LAZY_STATIC_PATHS.AVATARS, this.filename) + } + + return join(LAZY_STATIC_PATHS.BANNERS, this.filename) + } + + getPath () { + return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) + } + + removeImage () { + const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) + return remove(imagePath) + } +} diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts new file mode 100644 index 000000000..5cf6fb8f1 --- /dev/null +++ b/server/models/actor/actor.ts @@ -0,0 +1,699 @@ +import { values } from 'lodash' +import { extname } from 'path' +import { literal, Op, Transaction } from 'sequelize' +import { + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + DefaultScope, + ForeignKey, + HasMany, + HasOne, + Is, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { ModelCache } from '@server/models/model-cache' +import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' +import { ActorImage } from '../../../shared/models/actors/actor-image.model' +import { activityPubContextify } from '../../helpers/activitypub' +import { + isActorFollowersCountValid, + isActorFollowingCountValid, + isActorPreferredUsernameValid, + isActorPrivateKeyValid, + isActorPublicKeyValid +} from '../../helpers/custom-validators/activitypub/actor' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' +import { + ACTIVITY_PUB, + ACTIVITY_PUB_ACTOR_TYPES, + CONSTRAINTS_FIELDS, + MIMETYPES, + SERVER_ACTOR_NAME, + WEBSERVER +} from '../../initializers/constants' +import { + MActor, + MActorAccountChannelId, + MActorAPAccount, + MActorAPChannel, + MActorFormattable, + MActorFull, + MActorHost, + MActorServer, + MActorSummaryFormattable, + MActorUrl, + MActorWithInboxes +} from '../../types/models' +import { AccountModel } from '../account/account' +import { ServerModel } from '../server/server' +import { isOutdated, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoChannelModel } from '../video/video-channel' +import { ActorFollowModel } from './actor-follow' +import { ActorImageModel } from './actor-image' + +enum ScopeNames { + FULL = 'FULL' +} + +export const unusedActorAttributesForAPI = [ + 'publicKey', + 'privateKey', + 'inboxUrl', + 'outboxUrl', + 'sharedInboxUrl', + 'followersUrl', + 'followingUrl' +] + +@DefaultScope(() => ({ + include: [ + { + model: ServerModel, + required: false + }, + { + model: ActorImageModel, + as: 'Avatar', + required: false + } + ] +})) +@Scopes(() => ({ + [ScopeNames.FULL]: { + include: [ + { + model: AccountModel.unscoped(), + required: false + }, + { + model: VideoChannelModel.unscoped(), + required: false, + include: [ + { + model: AccountModel, + required: true + } + ] + }, + { + model: ServerModel, + required: false + }, + { + model: ActorImageModel, + as: 'Avatar', + required: false + }, + { + model: ActorImageModel, + as: 'Banner', + required: false + } + ] + } +})) +@Table({ + tableName: 'actor', + indexes: [ + { + fields: [ 'url' ], + unique: true + }, + { + fields: [ 'preferredUsername', 'serverId' ], + unique: true, + where: { + serverId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'preferredUsername' ], + unique: true, + where: { + serverId: null + } + }, + { + fields: [ 'inboxUrl', 'sharedInboxUrl' ] + }, + { + fields: [ 'sharedInboxUrl' ] + }, + { + fields: [ 'serverId' ] + }, + { + fields: [ 'avatarId' ] + }, + { + fields: [ 'followersUrl' ] + } + ] +}) +export class ActorModel extends Model { + + @AllowNull(false) + @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) + type: ActivityPubActorType + + @AllowNull(false) + @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username')) + @Column + preferredUsername: string + + @AllowNull(false) + @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + url: string + + @AllowNull(true) + @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max)) + publicKey: string + + @AllowNull(true) + @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max)) + privateKey: string + + @AllowNull(false) + @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count')) + @Column + followersCount: number + + @AllowNull(false) + @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count')) + @Column + followingCount: number + + @AllowNull(false) + @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + inboxUrl: string + + @AllowNull(true) + @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + outboxUrl: string + + @AllowNull(true) + @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + sharedInboxUrl: string + + @AllowNull(true) + @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + followersUrl: string + + @AllowNull(true) + @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) + followingUrl: string + + @AllowNull(true) + @Column + remoteCreatedAt: Date + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => ActorImageModel) + @Column + avatarId: number + + @ForeignKey(() => ActorImageModel) + @Column + bannerId: number + + @BelongsTo(() => ActorImageModel, { + foreignKey: { + name: 'avatarId', + allowNull: true + }, + as: 'Avatar', + onDelete: 'set null', + hooks: true + }) + Avatar: ActorImageModel + + @BelongsTo(() => ActorImageModel, { + foreignKey: { + name: 'bannerId', + allowNull: true + }, + as: 'Banner', + onDelete: 'set null', + hooks: true + }) + Banner: ActorImageModel + + @HasMany(() => ActorFollowModel, { + foreignKey: { + name: 'actorId', + allowNull: false + }, + as: 'ActorFollowings', + onDelete: 'cascade' + }) + ActorFollowing: ActorFollowModel[] + + @HasMany(() => ActorFollowModel, { + foreignKey: { + name: 'targetActorId', + allowNull: false + }, + as: 'ActorFollowers', + onDelete: 'cascade' + }) + ActorFollowers: ActorFollowModel[] + + @ForeignKey(() => ServerModel) + @Column + serverId: number + + @BelongsTo(() => ServerModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Server: ServerModel + + @HasOne(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade', + hooks: true + }) + Account: AccountModel + + @HasOne(() => VideoChannelModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade', + hooks: true + }) + VideoChannel: VideoChannelModel + + static load (id: number): Promise { + return ActorModel.unscoped().findByPk(id) + } + + static loadFull (id: number): Promise { + return ActorModel.scope(ScopeNames.FULL).findByPk(id) + } + + static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise { + const query = { + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoModel.unscoped(), + required: true, + where: { + id: videoId + } + } + ] + } + ] + } + ], + transaction + } + + return ActorModel.unscoped().findOne(query) + } + + static isActorUrlExist (url: string) { + const query = { + raw: true, + where: { + url + } + } + + return ActorModel.unscoped().findOne(query) + .then(a => !!a) + } + + static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise { + const query = { + where: { + followersUrl: { + [Op.in]: followersUrls + } + }, + transaction + } + + return ActorModel.scope(ScopeNames.FULL).findAll(query) + } + + static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise { + const fun = () => { + const query = { + where: { + preferredUsername, + serverId: null + }, + transaction + } + + return ActorModel.scope(ScopeNames.FULL) + .findOne(query) + } + + return ModelCache.Instance.doCache({ + cacheType: 'local-actor-name', + key: preferredUsername, + // The server actor never change, so we can easily cache it + whitelist: () => preferredUsername === SERVER_ACTOR_NAME, + fun + }) + } + + static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise { + const fun = () => { + const query = { + attributes: [ 'url' ], + where: { + preferredUsername, + serverId: null + }, + transaction + } + + return ActorModel.unscoped() + .findOne(query) + } + + return ModelCache.Instance.doCache({ + cacheType: 'local-actor-name', + key: preferredUsername, + // The server actor never change, so we can easily cache it + whitelist: () => preferredUsername === SERVER_ACTOR_NAME, + fun + }) + } + + static loadByNameAndHost (preferredUsername: string, host: string): Promise { + const query = { + where: { + preferredUsername + }, + include: [ + { + model: ServerModel, + required: true, + where: { + host + } + } + ] + } + + return ActorModel.scope(ScopeNames.FULL).findOne(query) + } + + static loadByUrl (url: string, transaction?: Transaction): Promise { + const query = { + where: { + url + }, + transaction, + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: false + }, + { + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + required: false + } + ] + } + + return ActorModel.unscoped().findOne(query) + } + + static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise { + const query = { + where: { + url + }, + transaction + } + + return ActorModel.scope(ScopeNames.FULL).findOne(query) + } + + static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) { + const sanitizedOfId = parseInt(ofId + '', 10) + const where = { id: sanitizedOfId } + + let columnToUpdate: string + let columnOfCount: string + + if (type === 'followers') { + columnToUpdate = 'followersCount' + columnOfCount = 'targetActorId' + } else { + columnToUpdate = 'followingCount' + columnOfCount = 'actorId' + } + + return ActorModel.update({ + [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`) + }, { where, transaction }) + } + + static loadAccountActorByVideoId (videoId: number): Promise { + const query = { + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'accountId' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'channelId' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + ] + } + + return ActorModel.unscoped().findOne(query) + } + + getSharedInbox (this: MActorWithInboxes) { + return this.sharedInboxUrl || this.inboxUrl + } + + toFormattedSummaryJSON (this: MActorSummaryFormattable) { + let avatar: ActorImage = null + if (this.Avatar) { + avatar = this.Avatar.toFormattedJSON() + } + + return { + url: this.url, + name: this.preferredUsername, + host: this.getHost(), + avatar + } + } + + toFormattedJSON (this: MActorFormattable) { + const base = this.toFormattedSummaryJSON() + + let banner: ActorImage = null + if (this.Banner) { + banner = this.Banner.toFormattedJSON() + } + + return Object.assign(base, { + id: this.id, + hostRedundancyAllowed: this.getRedundancyAllowed(), + followingCount: this.followingCount, + followersCount: this.followersCount, + banner, + createdAt: this.getCreatedAt() + }) + } + + toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { + let icon: ActivityIconObject + let image: ActivityIconObject + + if (this.avatarId) { + const extension = extname(this.Avatar.filename) + + icon = { + type: 'Image', + mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], + height: this.Avatar.height, + width: this.Avatar.width, + url: this.getAvatarUrl() + } + } + + if (this.bannerId) { + const banner = (this as MActorAPChannel).Banner + const extension = extname(banner.filename) + + image = { + type: 'Image', + mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], + height: banner.height, + width: banner.width, + url: this.getBannerUrl() + } + } + + const json = { + type: this.type, + id: this.url, + following: this.getFollowingUrl(), + followers: this.getFollowersUrl(), + playlists: this.getPlaylistsUrl(), + inbox: this.inboxUrl, + outbox: this.outboxUrl, + preferredUsername: this.preferredUsername, + url: this.url, + name, + endpoints: { + sharedInbox: this.sharedInboxUrl + }, + publicKey: { + id: this.getPublicKeyUrl(), + owner: this.url, + publicKeyPem: this.publicKey + }, + published: this.getCreatedAt().toISOString(), + icon, + image + } + + return activityPubContextify(json) + } + + getFollowerSharedInboxUrls (t: Transaction) { + const query = { + attributes: [ 'sharedInboxUrl' ], + include: [ + { + attribute: [], + model: ActorFollowModel.unscoped(), + required: true, + as: 'ActorFollowing', + where: { + state: 'accepted', + targetActorId: this.id + } + } + ], + transaction: t + } + + return ActorModel.findAll(query) + .then(accounts => accounts.map(a => a.sharedInboxUrl)) + } + + getFollowingUrl () { + return this.url + '/following' + } + + getFollowersUrl () { + return this.url + '/followers' + } + + getPlaylistsUrl () { + return this.url + '/playlists' + } + + getPublicKeyUrl () { + return this.url + '#main-key' + } + + isOwned () { + return this.serverId === null + } + + getWebfingerUrl (this: MActorServer) { + return 'acct:' + this.preferredUsername + '@' + this.getHost() + } + + getIdentifier () { + return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername + } + + getHost (this: MActorHost) { + return this.Server ? this.Server.host : WEBSERVER.HOST + } + + getRedundancyAllowed () { + return this.Server ? this.Server.redundancyAllowed : false + } + + getAvatarUrl () { + if (!this.avatarId) return undefined + + return WEBSERVER.URL + this.Avatar.getStaticPath() + } + + getBannerUrl () { + if (!this.bannerId) return undefined + + return WEBSERVER.URL + this.Banner.getStaticPath() + } + + isOutdated () { + if (this.isOwned()) return false + + return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL) + } + + getCreatedAt (this: MActorAPChannel | MActorAPAccount | MActorFormattable) { + return this.remoteCreatedAt || this.createdAt + } +} diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 27e643aa7..aa512a985 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -17,8 +17,8 @@ import { MUserAccountId } from '@server/types/models' import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' import { logger } from '../../helpers/logger' import { AccountModel } from '../account/account' -import { UserModel } from '../account/user' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' +import { UserModel } from '../user/user' import { OAuthClientModel } from './oauth-client' export type OAuthTokenInfo = { diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 53ebadeaf..b6538c8fd 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -29,7 +29,7 @@ import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validato import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' import { ServerModel } from '../server/server' import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 0e58beeaf..fb4c15d97 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts @@ -1,7 +1,7 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { MServer, MServerFormattable } from '@server/types/models/server' import { isHostValid } from '../../helpers/custom-validators/servers' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' import { throwIfNotValid } from '../utils' import { ServerBlocklistModel } from './server-blocklist' diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts new file mode 100644 index 000000000..138051528 --- /dev/null +++ b/server/models/user/user-notification-setting.ts @@ -0,0 +1,221 @@ +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BelongsTo, + Column, + CreatedAt, + Default, + ForeignKey, + Is, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { TokensCache } from '@server/lib/auth/tokens-cache' +import { MNotificationSettingFormattable } from '@server/types/models' +import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' +import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' +import { throwIfNotValid } from '../utils' +import { UserModel } from './user' + +@Table({ + tableName: 'userNotificationSetting', + indexes: [ + { + fields: [ 'userId' ], + unique: true + } + ] +}) +export class UserNotificationSettingModel extends Model { + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewVideoFromSubscription', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') + ) + @Column + newVideoFromSubscription: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewCommentOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') + ) + @Column + newCommentOnMyVideo: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator') + ) + @Column + abuseAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingVideoAutoBlacklistAsModerator', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator') + ) + @Column + videoAutoBlacklistAsModerator: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingBlacklistOnMyVideo', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') + ) + @Column + blacklistOnMyVideo: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoPublished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished') + ) + @Column + myVideoPublished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingMyVideoImportFinished', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished') + ) + @Column + myVideoImportFinished: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewUserRegistration', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration') + ) + @Column + newUserRegistration: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewInstanceFollower', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower') + ) + @Column + newInstanceFollower: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewInstanceFollower', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing') + ) + @Column + autoInstanceFollowing: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewFollow', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') + ) + @Column + newFollow: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingCommentMention', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention') + ) + @Column + commentMention: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseStateChange', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange') + ) + @Column + abuseStateChange: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingAbuseNewMessage', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage') + ) + @Column + abuseNewMessage: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewPeerTubeVersion', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion') + ) + @Column + newPeerTubeVersion: UserNotificationSettingValue + + @AllowNull(false) + @Default(null) + @Is( + 'UserNotificationSettingNewPeerPluginVersion', + value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion') + ) + @Column + newPluginVersion: UserNotificationSettingValue + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserNotificationSettingModel) { + return TokensCache.Instance.clearCacheByUserId(instance.userId) + } + + toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { + return { + newCommentOnMyVideo: this.newCommentOnMyVideo, + newVideoFromSubscription: this.newVideoFromSubscription, + abuseAsModerator: this.abuseAsModerator, + videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, + blacklistOnMyVideo: this.blacklistOnMyVideo, + myVideoPublished: this.myVideoPublished, + myVideoImportFinished: this.myVideoImportFinished, + newUserRegistration: this.newUserRegistration, + commentMention: this.commentMention, + newFollow: this.newFollow, + newInstanceFollower: this.newInstanceFollower, + autoInstanceFollowing: this.autoInstanceFollowing, + abuseNewMessage: this.abuseNewMessage, + abuseStateChange: this.abuseStateChange, + newPeerTubeVersion: this.newPeerTubeVersion, + newPluginVersion: this.newPluginVersion + } + } +} diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts new file mode 100644 index 000000000..f7f9ac867 --- /dev/null +++ b/server/models/user/user-notification.ts @@ -0,0 +1,665 @@ +import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' +import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' +import { UserNotification, UserNotificationType } from '../../../shared' +import { isBooleanValid } from '../../helpers/custom-validators/misc' +import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' +import { AbuseModel } from '../abuse/abuse' +import { VideoAbuseModel } from '../abuse/video-abuse' +import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' +import { AccountModel } from '../account/account' +import { ActorModel } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' +import { ApplicationModel } from '../application/application' +import { PluginModel } from '../server/plugin' +import { ServerModel } from '../server/server' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoBlacklistModel } from '../video/video-blacklist' +import { VideoChannelModel } from '../video/video-channel' +import { VideoCommentModel } from '../video/video-comment' +import { VideoImportModel } from '../video/video-import' +import { UserModel } from './user' + +enum ScopeNames { + WITH_ALL = 'WITH_ALL' +} + +function buildActorWithAvatarInclude () { + return { + attributes: [ 'preferredUsername' ], + model: ActorModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'filename' ], + as: 'Avatar', + model: ActorImageModel.unscoped(), + required: false + }, + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + } +} + +function buildVideoInclude (required: boolean) { + return { + attributes: [ 'id', 'uuid', 'name' ], + model: VideoModel.unscoped(), + required + } +} + +function buildChannelInclude (required: boolean, withActor = false) { + return { + required, + attributes: [ 'id', 'name' ], + model: VideoChannelModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] + } +} + +function buildAccountInclude (required: boolean, withActor = false) { + return { + required, + attributes: [ 'id', 'name' ], + model: AccountModel.unscoped(), + include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] + } +} + +@Scopes(() => ({ + [ScopeNames.WITH_ALL]: { + include: [ + Object.assign(buildVideoInclude(false), { + include: [ buildChannelInclude(true, true) ] + }), + + { + attributes: [ 'id', 'originCommentId' ], + model: VideoCommentModel.unscoped(), + required: false, + include: [ + buildAccountInclude(true, true), + buildVideoInclude(true) + ] + }, + + { + attributes: [ 'id', 'state' ], + model: AbuseModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id' ], + model: VideoAbuseModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] + }, + { + attributes: [ 'id' ], + model: VideoCommentAbuseModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'originCommentId' ], + model: VideoCommentModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'id', 'name', 'uuid' ], + model: VideoModel.unscoped(), + required: false + } + ] + } + ] + }, + { + model: AccountModel, + as: 'FlaggedAccount', + required: false, + include: [ buildActorWithAvatarInclude() ] + } + ] + }, + + { + attributes: [ 'id' ], + model: VideoBlacklistModel.unscoped(), + required: false, + include: [ buildVideoInclude(true) ] + }, + + { + attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], + model: VideoImportModel.unscoped(), + required: false, + include: [ buildVideoInclude(false) ] + }, + + { + attributes: [ 'id', 'name', 'type', 'latestVersion' ], + model: PluginModel.unscoped(), + required: false + }, + + { + attributes: [ 'id', 'latestPeerTubeVersion' ], + model: ApplicationModel.unscoped(), + required: false + }, + + { + attributes: [ 'id', 'state' ], + model: ActorFollowModel.unscoped(), + required: false, + include: [ + { + attributes: [ 'preferredUsername' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollower', + include: [ + { + attributes: [ 'id', 'name' ], + model: AccountModel.unscoped(), + required: true + }, + { + attributes: [ 'filename' ], + as: 'Avatar', + model: ActorImageModel.unscoped(), + required: false + }, + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + }, + { + attributes: [ 'preferredUsername', 'type' ], + model: ActorModel.unscoped(), + required: true, + as: 'ActorFollowing', + include: [ + buildChannelInclude(false), + buildAccountInclude(false), + { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + ] + } + ] + }, + + buildAccountInclude(false, true) + ] + } +})) +@Table({ + tableName: 'userNotification', + indexes: [ + { + fields: [ 'userId' ] + }, + { + fields: [ 'videoId' ], + where: { + videoId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'commentId' ], + where: { + commentId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'abuseId' ], + where: { + abuseId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoBlacklistId' ], + where: { + videoBlacklistId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'videoImportId' ], + where: { + videoImportId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'accountId' ], + where: { + accountId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'actorFollowId' ], + where: { + actorFollowId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'pluginId' ], + where: { + pluginId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'applicationId' ], + where: { + applicationId: { + [Op.ne]: null + } + } + } + ] as (ModelIndexesOptions & { where?: WhereOptions })[] +}) +export class UserNotificationModel extends Model { + + @AllowNull(false) + @Default(null) + @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) + @Column + type: UserNotificationType + + @AllowNull(false) + @Default(false) + @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) + @Column + read: boolean + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + User: UserModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Video: VideoModel + + @ForeignKey(() => VideoCommentModel) + @Column + commentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Comment: VideoCommentModel + + @ForeignKey(() => AbuseModel) + @Column + abuseId: number + + @BelongsTo(() => AbuseModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Abuse: AbuseModel + + @ForeignKey(() => VideoBlacklistModel) + @Column + videoBlacklistId: number + + @BelongsTo(() => VideoBlacklistModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoBlacklist: VideoBlacklistModel + + @ForeignKey(() => VideoImportModel) + @Column + videoImportId: number + + @BelongsTo(() => VideoImportModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + VideoImport: VideoImportModel + + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Account: AccountModel + + @ForeignKey(() => ActorFollowModel) + @Column + actorFollowId: number + + @BelongsTo(() => ActorFollowModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + ActorFollow: ActorFollowModel + + @ForeignKey(() => PluginModel) + @Column + pluginId: number + + @BelongsTo(() => PluginModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Plugin: PluginModel + + @ForeignKey(() => ApplicationModel) + @Column + applicationId: number + + @BelongsTo(() => ApplicationModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Application: ApplicationModel + + static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { + const where = { userId } + + const query: FindOptions = { + offset: start, + limit: count, + order: getSort(sort), + where + } + + if (unread !== undefined) query.where['read'] = !unread + + return Promise.all([ + UserNotificationModel.count({ where }) + .then(count => count || 0), + + count === 0 + ? [] + : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) + ]).then(([ total, data ]) => ({ total, data })) + } + + static markAsRead (userId: number, notificationIds: number[]) { + const query = { + where: { + userId, + id: { + [Op.in]: notificationIds + } + } + } + + return UserNotificationModel.update({ read: true }, query) + } + + static markAllAsRead (userId: number) { + const query = { where: { userId } } + + return UserNotificationModel.update({ read: true }, query) + } + + static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) { + const id = parseInt(options.id + '', 10) + + function buildAccountWhereQuery (base: string) { + const whereSuffix = options.forUserId + ? ` AND "userNotification"."userId" = ${options.forUserId}` + : '' + + if (options.type === 'account') { + return base + + ` WHERE "account"."id" = ${id} ${whereSuffix}` + } + + return base + + ` WHERE "actor"."serverId" = ${id} ${whereSuffix}` + } + + const queries = [ + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ), + + // Remove notifications from muted accounts that followed ours + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + // Remove notifications from muted accounts that commented something + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + + `INNER JOIN account ON account.id = "videoComment"."accountId" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ) + ] + + const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})` + + return UserNotificationModel.sequelize.query(query) + } + + toFormattedJSON (this: UserNotificationModelForApi): UserNotification { + const video = this.Video + ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) + : undefined + + const videoImport = this.VideoImport + ? { + id: this.VideoImport.id, + video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, + torrentName: this.VideoImport.torrentName, + magnetUri: this.VideoImport.magnetUri, + targetUrl: this.VideoImport.targetUrl + } + : undefined + + const comment = this.Comment + ? { + id: this.Comment.id, + threadId: this.Comment.getThreadId(), + account: this.formatActor(this.Comment.Account), + video: this.formatVideo(this.Comment.Video) + } + : undefined + + const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined + + const videoBlacklist = this.VideoBlacklist + ? { + id: this.VideoBlacklist.id, + video: this.formatVideo(this.VideoBlacklist.Video) + } + : undefined + + const account = this.Account ? this.formatActor(this.Account) : undefined + + const actorFollowingType = { + Application: 'instance' as 'instance', + Group: 'channel' as 'channel', + Person: 'account' as 'account' + } + const actorFollow = this.ActorFollow + ? { + id: this.ActorFollow.id, + state: this.ActorFollow.state, + follower: { + id: this.ActorFollow.ActorFollower.Account.id, + displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), + name: this.ActorFollow.ActorFollower.preferredUsername, + avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, + host: this.ActorFollow.ActorFollower.getHost() + }, + following: { + type: actorFollowingType[this.ActorFollow.ActorFollowing.type], + displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), + name: this.ActorFollow.ActorFollowing.preferredUsername, + host: this.ActorFollow.ActorFollowing.getHost() + } + } + : undefined + + const plugin = this.Plugin + ? { + name: this.Plugin.name, + type: this.Plugin.type, + latestVersion: this.Plugin.latestVersion + } + : undefined + + const peertube = this.Application + ? { latestVersion: this.Application.latestPeerTubeVersion } + : undefined + + return { + id: this.id, + type: this.type, + read: this.read, + video, + videoImport, + comment, + abuse, + videoBlacklist, + account, + actorFollow, + plugin, + peertube, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString() + } + } + + formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { + return { + id: video.id, + uuid: video.uuid, + name: video.name + } + } + + formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { + const commentAbuse = abuse.VideoCommentAbuse?.VideoComment + ? { + threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), + + video: abuse.VideoCommentAbuse.VideoComment.Video + ? { + id: abuse.VideoCommentAbuse.VideoComment.Video.id, + name: abuse.VideoCommentAbuse.VideoComment.Video.name, + uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid + } + : undefined + } + : undefined + + const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined + + const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined + + return { + id: abuse.id, + state: abuse.state, + video: videoAbuse, + comment: commentAbuse, + account: accountAbuse + } + } + + formatActor ( + this: UserNotificationModelForApi, + accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor + ) { + const avatar = accountOrChannel.Actor.Avatar + ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } + : undefined + + return { + id: accountOrChannel.id, + displayName: accountOrChannel.getDisplayName(), + name: accountOrChannel.Actor.preferredUsername, + host: accountOrChannel.Actor.getHost(), + avatar + } + } +} diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts new file mode 100644 index 000000000..6be1d65ea --- /dev/null +++ b/server/models/user/user-video-history.ts @@ -0,0 +1,100 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoModel } from '../video/video' +import { UserModel } from './user' +import { DestroyOptions, Op, Transaction } from 'sequelize' +import { MUserAccountId, MUserId } from '@server/types/models' + +@Table({ + tableName: 'userVideoHistory', + indexes: [ + { + fields: [ 'userId', 'videoId' ], + unique: true + }, + { + fields: [ 'userId' ] + }, + { + fields: [ 'videoId' ] + } + ] +}) +export class UserVideoHistoryModel extends Model { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @IsInt + @Column + currentTime: number + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + Video: VideoModel + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + User: UserModel + + static listForApi (user: MUserAccountId, start: number, count: number, search?: string) { + return VideoModel.listForApi({ + start, + count, + search, + sort: '-"userVideoHistory"."updatedAt"', + nsfw: null, // All + includeLocalVideos: true, + withFiles: false, + user, + historyOfUser: user + }) + } + + static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) { + const query: DestroyOptions = { + where: { + userId: user.id + }, + transaction: t + } + + if (beforeDate) { + query.where['updatedAt'] = { + [Op.lt]: beforeDate + } + } + + return UserVideoHistoryModel.destroy(query) + } + + static removeOldHistory (beforeDate: string) { + const query: DestroyOptions = { + where: { + updatedAt: { + [Op.lt]: beforeDate + } + } + } + + return UserVideoHistoryModel.destroy(query) + } +} diff --git a/server/models/user/user.ts b/server/models/user/user.ts new file mode 100644 index 000000000..8d2564e54 --- /dev/null +++ b/server/models/user/user.ts @@ -0,0 +1,967 @@ +import { values } from 'lodash' +import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize' +import { + AfterDestroy, + AfterUpdate, + AllowNull, + BeforeCreate, + BeforeUpdate, + Column, + CreatedAt, + DataType, + Default, + DefaultScope, + HasMany, + HasOne, + Is, + IsEmail, + IsUUID, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { TokensCache } from '@server/lib/auth/tokens-cache' +import { + MMyUserFormattable, + MUser, + MUserDefault, + MUserFormattable, + MUserNotifSettingChannelDefault, + MUserWithNotificationSetting, + MVideoWithRights +} from '@server/types/models' +import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' +import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' +import { User, UserRole } from '../../../shared/models/users' +import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' +import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' +import { isThemeNameValid } from '../../helpers/custom-validators/plugins' +import { + isNoInstanceConfigWarningModal, + isNoWelcomeModal, + isUserAdminFlagsValid, + isUserAutoPlayNextVideoPlaylistValid, + isUserAutoPlayNextVideoValid, + isUserAutoPlayVideoValid, + isUserBlockedReasonValid, + isUserBlockedValid, + isUserEmailVerifiedValid, + isUserNSFWPolicyValid, + isUserPasswordValid, + isUserRoleValid, + isUserUsernameValid, + isUserVideoLanguages, + isUserVideoQuotaDailyValid, + isUserVideoQuotaValid, + isUserVideosHistoryEnabledValid, + isUserWebTorrentEnabledValid +} from '../../helpers/custom-validators/users' +import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' +import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' +import { AccountModel } from '../account/account' +import { ActorModel } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' +import { OAuthTokenModel } from '../oauth/oauth-token' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' +import { VideoChannelModel } from '../video/video-channel' +import { VideoImportModel } from '../video/video-import' +import { VideoLiveModel } from '../video/video-live' +import { VideoPlaylistModel } from '../video/video-playlist' +import { UserNotificationSettingModel } from './user-notification-setting' + +enum ScopeNames { + FOR_ME_API = 'FOR_ME_API', + WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', + WITH_STATS = 'WITH_STATS' +} + +@DefaultScope(() => ({ + include: [ + { + model: AccountModel, + required: true + }, + { + model: UserNotificationSettingModel, + required: true + } + ] +})) +@Scopes(() => ({ + [ScopeNames.FOR_ME_API]: { + include: [ + { + model: AccountModel, + include: [ + { + model: VideoChannelModel.unscoped(), + include: [ + { + model: ActorModel, + required: true, + include: [ + { + model: ActorImageModel, + as: 'Banner', + required: false + } + ] + } + ] + }, + { + attributes: [ 'id', 'name', 'type' ], + model: VideoPlaylistModel.unscoped(), + required: true, + where: { + type: { + [Op.ne]: VideoPlaylistType.REGULAR + } + } + } + ] + }, + { + model: UserNotificationSettingModel, + required: true + } + ] + }, + [ScopeNames.WITH_VIDEOCHANNELS]: { + include: [ + { + model: AccountModel, + include: [ + { + model: VideoChannelModel + }, + { + attributes: [ 'id', 'name', 'type' ], + model: VideoPlaylistModel.unscoped(), + required: true, + where: { + type: { + [Op.ne]: VideoPlaylistType.REGULAR + } + } + } + ] + } + ] + }, + [ScopeNames.WITH_STATS]: { + attributes: { + include: [ + [ + literal( + '(' + + UserModel.generateUserQuotaBaseSQL({ + withSelect: false, + whereUserId: '"UserModel"."id"' + }) + + ')' + ), + 'videoQuotaUsed' + ], + [ + literal( + '(' + + 'SELECT COUNT("video"."id") ' + + 'FROM "video" ' + + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ')' + ), + 'videosCount' + ], + [ + literal( + '(' + + `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + + 'FROM (' + + 'SELECT COUNT("abuse"."id") AS "abuses", ' + + `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ') t' + + ')' + ), + 'abusesCount' + ], + [ + literal( + '(' + + 'SELECT COUNT("abuse"."id") ' + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ')' + ), + 'abusesCreatedCount' + ], + [ + literal( + '(' + + 'SELECT COUNT("videoComment"."id") ' + + 'FROM "videoComment" ' + + 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' + + 'WHERE "account"."userId" = "UserModel"."id"' + + ')' + ), + 'videoCommentsCount' + ] + ] + } + } +})) +@Table({ + tableName: 'user', + indexes: [ + { + fields: [ 'username' ], + unique: true + }, + { + fields: [ 'email' ], + unique: true + } + ] +}) +export class UserModel extends Model { + + @AllowNull(true) + @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) + @Column + password: string + + @AllowNull(false) + @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name')) + @Column + username: string + + @AllowNull(false) + @IsEmail + @Column(DataType.STRING(400)) + email: string + + @AllowNull(true) + @IsEmail + @Column(DataType.STRING(400)) + pendingEmail: string + + @AllowNull(true) + @Default(null) + @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) + @Column + emailVerified: boolean + + @AllowNull(false) + @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) + @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES))) + nsfwPolicy: NSFWPolicyType + + @AllowNull(false) + @Default(true) + @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) + @Column + webTorrentEnabled: boolean + + @AllowNull(false) + @Default(true) + @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled')) + @Column + videosHistoryEnabled: boolean + + @AllowNull(false) + @Default(true) + @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) + @Column + autoPlayVideo: boolean + + @AllowNull(false) + @Default(false) + @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean')) + @Column + autoPlayNextVideo: boolean + + @AllowNull(false) + @Default(true) + @Is( + 'UserAutoPlayNextVideoPlaylist', + value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean') + ) + @Column + autoPlayNextVideoPlaylist: boolean + + @AllowNull(true) + @Default(null) + @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages')) + @Column(DataType.ARRAY(DataType.STRING)) + videoLanguages: string[] + + @AllowNull(false) + @Default(UserAdminFlag.NONE) + @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags')) + @Column + adminFlags?: UserAdminFlag + + @AllowNull(false) + @Default(false) + @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean')) + @Column + blocked: boolean + + @AllowNull(true) + @Default(null) + @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true)) + @Column + blockedReason: string + + @AllowNull(false) + @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role')) + @Column + role: number + + @AllowNull(false) + @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota')) + @Column(DataType.BIGINT) + videoQuota: number + + @AllowNull(false) + @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily')) + @Column(DataType.BIGINT) + videoQuotaDaily: number + + @AllowNull(false) + @Default(DEFAULT_USER_THEME_NAME) + @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme')) + @Column + theme: string + + @AllowNull(false) + @Default(false) + @Is( + 'UserNoInstanceConfigWarningModal', + value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal') + ) + @Column + noInstanceConfigWarningModal: boolean + + @AllowNull(false) + @Default(false) + @Is( + 'UserNoInstanceConfigWarningModal', + value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal') + ) + @Column + noWelcomeModal: boolean + + @AllowNull(true) + @Default(null) + @Column + pluginAuth: string + + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + feedToken: string + + @AllowNull(true) + @Default(null) + @Column + lastLoginDate: Date + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @HasOne(() => AccountModel, { + foreignKey: 'userId', + onDelete: 'cascade', + hooks: true + }) + Account: AccountModel + + @HasOne(() => UserNotificationSettingModel, { + foreignKey: 'userId', + onDelete: 'cascade', + hooks: true + }) + NotificationSetting: UserNotificationSettingModel + + @HasMany(() => VideoImportModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + VideoImports: VideoImportModel[] + + @HasMany(() => OAuthTokenModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + OAuthTokens: OAuthTokenModel[] + + @BeforeCreate + @BeforeUpdate + static cryptPasswordIfNeeded (instance: UserModel) { + if (instance.changed('password') && instance.password) { + return cryptPassword(instance.password) + .then(hash => { + instance.password = hash + return undefined + }) + } + } + + @AfterUpdate + @AfterDestroy + static removeTokenCache (instance: UserModel) { + return TokensCache.Instance.clearCacheByUserId(instance.id) + } + + static countTotal () { + return this.count() + } + + static listForApi (parameters: { + start: number + count: number + sort: string + search?: string + blocked?: boolean + }) { + const { start, count, sort, search, blocked } = parameters + const where: WhereOptions = {} + + if (search) { + Object.assign(where, { + [Op.or]: [ + { + email: { + [Op.iLike]: '%' + search + '%' + } + }, + { + username: { + [Op.iLike]: '%' + search + '%' + } + } + ] + }) + } + + if (blocked !== undefined) { + Object.assign(where, { + blocked: blocked + }) + } + + const query: FindOptions = { + attributes: { + include: [ + [ + literal( + '(' + + UserModel.generateUserQuotaBaseSQL({ + withSelect: false, + whereUserId: '"UserModel"."id"' + }) + + ')' + ), + 'videoQuotaUsed' + ] as any // FIXME: typings + ] + }, + offset: start, + limit: count, + order: getSort(sort), + where + } + + return UserModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listWithRight (right: UserRight): Promise { + const roles = Object.keys(USER_ROLE_LABELS) + .map(k => parseInt(k, 10) as UserRole) + .filter(role => hasUserRight(role, right)) + + const query = { + where: { + role: { + [Op.in]: roles + } + } + } + + return UserModel.findAll(query) + } + + static listUserSubscribersOf (actorId: number): Promise { + const query = { + include: [ + { + model: UserNotificationSettingModel.unscoped(), + required: true + }, + { + attributes: [ 'userId' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [], + model: ActorModel.unscoped(), + required: true, + where: { + serverId: null + }, + include: [ + { + attributes: [], + as: 'ActorFollowings', + model: ActorFollowModel.unscoped(), + required: true, + where: { + targetActorId: actorId + } + } + ] + } + ] + } + ] + } + + return UserModel.unscoped().findAll(query) + } + + static listByUsernames (usernames: string[]): Promise { + const query = { + where: { + username: usernames + } + } + + return UserModel.findAll(query) + } + + static loadById (id: number): Promise { + return UserModel.unscoped().findByPk(id) + } + + static loadByIdFull (id: number): Promise { + return UserModel.findByPk(id) + } + + static loadByIdWithChannels (id: number, withStats = false): Promise { + const scopes = [ + ScopeNames.WITH_VIDEOCHANNELS + ] + + if (withStats) scopes.push(ScopeNames.WITH_STATS) + + return UserModel.scope(scopes).findByPk(id) + } + + static loadByUsername (username: string): Promise { + const query = { + where: { + username + } + } + + return UserModel.findOne(query) + } + + static loadForMeAPI (id: number): Promise { + const query = { + where: { + id + } + } + + return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query) + } + + static loadByEmail (email: string): Promise { + const query = { + where: { + email + } + } + + return UserModel.findOne(query) + } + + static loadByUsernameOrEmail (username: string, email?: string): Promise { + if (!email) email = username + + const query = { + where: { + [Op.or]: [ + where(fn('lower', col('username')), fn('lower', username)), + + { email } + ] + } + } + + return UserModel.findOne(query) + } + + static loadByVideoId (videoId: number): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByVideoImportId (videoImportId: number): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoImportModel.unscoped(), + where: { + id: videoImportId + } + } + ] + } + + return UserModel.findOne(query) + } + + static loadByChannelActorId (videoChannelActorId: number): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + include: [ + { + required: true, + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + where: { + actorId: videoChannelActorId + } + } + ] + } + ] + } + + return UserModel.findOne(query) + } + + static loadByAccountActorId (accountActorId: number): Promise { + const query = { + include: [ + { + required: true, + attributes: [ 'id' ], + model: AccountModel.unscoped(), + where: { + actorId: accountActorId + } + } + ] + } + + return UserModel.findOne(query) + } + + static loadByLiveId (liveId: number): Promise { + const query = { + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id' ], + model: VideoModel.unscoped(), + required: true, + include: [ + { + attributes: [], + model: VideoLiveModel.unscoped(), + required: true, + where: { + id: liveId + } + } + ] + } + ] + } + ] + } + ] + } + + return UserModel.unscoped().findOne(query) + } + + static generateUserQuotaBaseSQL (options: { + whereUserId: '$userId' | '"UserModel"."id"' + withSelect: boolean + where?: string + }) { + const andWhere = options.where + ? 'AND ' + options.where + : '' + + const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + + `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}` + + const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + + 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + + videoChannelJoin + + const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' + + 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' + + 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' + + videoChannelJoin + + return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' + + 'FROM (' + + `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` + + 'GROUP BY "t1"."videoId"' + + ') t2' + } + + static getTotalRawQuery (query: string, userId: number) { + const options = { + bind: { userId }, + type: QueryTypes.SELECT as QueryTypes.SELECT + } + + return UserModel.sequelize.query<{ total: string }>(query, options) + .then(([ { total } ]) => { + if (total === null) return 0 + + return parseInt(total, 10) + }) + } + + static async getStats () { + function getActiveUsers (days: number) { + const query = { + where: { + [Op.and]: [ + literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`) + ] + } + } + + return UserModel.count(query) + } + + const totalUsers = await UserModel.count() + const totalDailyActiveUsers = await getActiveUsers(1) + const totalWeeklyActiveUsers = await getActiveUsers(7) + const totalMonthlyActiveUsers = await getActiveUsers(30) + const totalHalfYearActiveUsers = await getActiveUsers(180) + + return { + totalUsers, + totalDailyActiveUsers, + totalWeeklyActiveUsers, + totalMonthlyActiveUsers, + totalHalfYearActiveUsers + } + } + + static autoComplete (search: string) { + const query = { + where: { + username: { + [Op.like]: `%${search}%` + } + }, + limit: 10 + } + + return UserModel.findAll(query) + .then(u => u.map(u => u.username)) + } + + canGetVideo (video: MVideoWithRights) { + const videoUserId = video.VideoChannel.Account.userId + + if (video.isBlacklisted()) { + return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + if (video.privacy === VideoPrivacy.PRIVATE) { + return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + if (video.privacy === VideoPrivacy.INTERNAL) return true + + return false + } + + hasRight (right: UserRight) { + return hasUserRight(this.role, right) + } + + hasAdminFlag (flag: UserAdminFlag) { + return this.adminFlags & flag + } + + isPasswordMatch (password: string) { + return comparePassword(password, this.password) + } + + toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { + const videoQuotaUsed = this.get('videoQuotaUsed') + const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') + const videosCount = this.get('videosCount') + const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') + const abusesCreatedCount = this.get('abusesCreatedCount') + const videoCommentsCount = this.get('videoCommentsCount') + + const json: User = { + id: this.id, + username: this.username, + email: this.email, + theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME), + + pendingEmail: this.pendingEmail, + emailVerified: this.emailVerified, + + nsfwPolicy: this.nsfwPolicy, + webTorrentEnabled: this.webTorrentEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, + autoPlayVideo: this.autoPlayVideo, + autoPlayNextVideo: this.autoPlayNextVideo, + autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, + videoLanguages: this.videoLanguages, + + role: this.role, + roleLabel: USER_ROLE_LABELS[this.role], + + videoQuota: this.videoQuota, + videoQuotaDaily: this.videoQuotaDaily, + videoQuotaUsed: videoQuotaUsed !== undefined + ? parseInt(videoQuotaUsed + '', 10) + : undefined, + videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined + ? parseInt(videoQuotaUsedDaily + '', 10) + : undefined, + videosCount: videosCount !== undefined + ? parseInt(videosCount + '', 10) + : undefined, + abusesCount: abusesCount + ? parseInt(abusesCount, 10) + : undefined, + abusesAcceptedCount: abusesAcceptedCount + ? parseInt(abusesAcceptedCount, 10) + : undefined, + abusesCreatedCount: abusesCreatedCount !== undefined + ? parseInt(abusesCreatedCount + '', 10) + : undefined, + videoCommentsCount: videoCommentsCount !== undefined + ? parseInt(videoCommentsCount + '', 10) + : undefined, + + noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, + noWelcomeModal: this.noWelcomeModal, + + blocked: this.blocked, + blockedReason: this.blockedReason, + + account: this.Account.toFormattedJSON(), + + notificationSettings: this.NotificationSetting + ? this.NotificationSetting.toFormattedJSON() + : undefined, + + videoChannels: [], + + createdAt: this.createdAt, + + pluginAuth: this.pluginAuth, + + lastLoginDate: this.lastLoginDate + } + + if (parameters.withAdminFlags) { + Object.assign(json, { adminFlags: this.adminFlags }) + } + + if (Array.isArray(this.Account.VideoChannels) === true) { + json.videoChannels = this.Account.VideoChannels + .map(c => c.toFormattedJSON()) + .sort((v1, v2) => { + if (v1.createdAt < v2.createdAt) return -1 + if (v1.createdAt === v2.createdAt) return 0 + + return 1 + }) + } + + return json + } + + toMeFormattedJSON (this: MMyUserFormattable): MyUser { + const formatted = this.toFormattedJSON() + + const specialPlaylists = this.Account.VideoPlaylists + .map(p => ({ id: p.id, name: p.name, type: p.type })) + + return Object.assign(formatted, { specialPlaylists }) + } +} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 081b21f2d..8e4b78723 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -36,9 +36,9 @@ import { MChannelSummaryFormattable } from '../../types/models/video' import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' -import { ActorImageModel } from '../account/actor-image' -import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' -import { ActorFollowModel } from '../activitypub/actor-follow' +import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' +import { ActorFollowModel } from '../actor/actor-follow' +import { ActorImageModel } from '../actor/actor-image' import { ServerModel } from '../server/server' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 151c2bc81..87c66dc9f 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -39,7 +39,7 @@ import { } from '../../types/models/video' import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' import { AccountModel } from '../account/account' -import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' +import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' import { buildBlockedAccountSQL, buildBlockedAccountSQLOptimized, diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8324166cc..d8f09e1e5 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts @@ -13,15 +13,15 @@ import { Table, UpdatedAt } from 'sequelize-typescript' +import { afterCommitIfTransaction } from '@server/helpers/database-utils' import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import' import { VideoImport, VideoImportState } from '../../../shared' import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' -import { UserModel } from '../account/user' +import { UserModel } from '../user/user' import { getSort, throwIfNotValid } from '../utils' import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' -import { afterCommitIfTransaction } from '@server/helpers/database-utils' @DefaultScope(() => ({ include: [ diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index efe5be36d..b48dd2945 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -50,11 +50,11 @@ import { MVideoPlaylistIdWithElements } from '../../types/models/video/video-playlist' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' +import { ActorModel } from '../actor/actor' import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' import { ThumbnailModel } from './thumbnail' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' import { VideoPlaylistElementModel } from './video-playlist-element' -import { ActorModel } from '../activitypub/actor' enum ScopeNames { AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 5059c1fa6..99a24dbdf 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -4,7 +4,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { MActorDefault } from '../../types/models' import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' import { VideoModel } from './video' diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8c316e00c..f8a099d9c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -100,14 +100,14 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models import { VideoAbuseModel } from '../abuse/video-abuse' import { AccountModel } from '../account/account' import { AccountVideoRateModel } from '../account/account-video-rate' -import { ActorImageModel } from '../account/actor-image' -import { UserModel } from '../account/user' -import { UserVideoHistoryModel } from '../account/user-video-history' -import { ActorModel } from '../activitypub/actor' +import { ActorModel } from '../actor/actor' +import { ActorImageModel } from '../actor/actor-image' import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { ServerModel } from '../server/server' import { TrackerModel } from '../server/tracker' import { VideoTrackerModel } from '../server/video-tracker' +import { UserModel } from '../user/user' +import { UserVideoHistoryModel } from '../user/user-video-history' import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' import { ScheduleVideoUpdateModel } from './schedule-video-update' import { TagModel } from './tag' diff --git a/server/types/models/abuse/abuse-message.ts b/server/types/models/abuse/abuse-message.ts new file mode 100644 index 000000000..565eca706 --- /dev/null +++ b/server/types/models/abuse/abuse-message.ts @@ -0,0 +1,20 @@ +import { AbuseMessageModel } from '@server/models/abuse/abuse-message' +import { PickWith } from '@shared/core-utils' +import { AbuseModel } from '../../../models/abuse/abuse' +import { MAccountFormattable } from '../account' + +type Use = PickWith + +// ############################################################################ + +export type MAbuseMessage = Omit + +export type MAbuseMessageId = Pick + +// ############################################################################ + +// Format for API + +export type MAbuseMessageFormattable = + MAbuseMessage & + Use<'Account', MAccountFormattable> diff --git a/server/types/models/abuse/abuse.ts b/server/types/models/abuse/abuse.ts new file mode 100644 index 000000000..6fd83684c --- /dev/null +++ b/server/types/models/abuse/abuse.ts @@ -0,0 +1,114 @@ +import { VideoAbuseModel } from '@server/models/abuse/video-abuse' +import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' +import { VideoCommentModel } from '@server/models/video/video-comment' +import { PickWith } from '@shared/core-utils' +import { AbuseModel } from '../../../models/abuse/abuse' +import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' +import { MComment, MCommentOwner, MCommentUrl, MCommentVideo, MVideoUrl } from '../video' +import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' + +type Use = PickWith +type UseVideoAbuse = PickWith +type UseCommentAbuse = PickWith + +// ############################################################################ + +export type MAbuse = Omit + +export type MVideoAbuse = Omit + +export type MCommentAbuse = Omit + +export type MAbuseReporter = + MAbuse & + Use<'ReporterAccount', MAccountDefault> + +// ############################################################################ + +export type MVideoAbuseVideo = + MVideoAbuse & + UseVideoAbuse<'Video', MVideo> + +export type MVideoAbuseVideoUrl = + MVideoAbuse & + UseVideoAbuse<'Video', MVideoUrl> + +export type MVideoAbuseVideoFull = + MVideoAbuse & + UseVideoAbuse<'Video', Omit> + +export type MVideoAbuseFormattable = + MVideoAbuse & + UseVideoAbuse<'Video', Pick> + +// ############################################################################ + +export type MCommentAbuseAccount = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentOwner> + +export type MCommentAbuseAccountVideo = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentOwner & PickWith> + +export type MCommentAbuseUrl = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MCommentUrl> + +export type MCommentAbuseFormattable = + MCommentAbuse & + UseCommentAbuse<'VideoComment', MComment & PickWith>> + +// ############################################################################ + +export type MAbuseId = Pick + +export type MAbuseVideo = + MAbuse & + Pick & + Use<'VideoAbuse', MVideoAbuseVideo> + +export type MAbuseUrl = + MAbuse & + Use<'VideoAbuse', MVideoAbuseVideoUrl> & + Use<'VideoCommentAbuse', MCommentAbuseUrl> + +export type MAbuseAccountVideo = + MAbuse & + Pick & + Use<'VideoAbuse', MVideoAbuseVideoFull> & + Use<'ReporterAccount', MAccountDefault> + +export type MAbuseFull = + MAbuse & + Pick & + Use<'ReporterAccount', MAccountLight> & + Use<'FlaggedAccount', MAccountLight> & + Use<'VideoAbuse', MVideoAbuseVideoFull> & + Use<'VideoCommentAbuse', MCommentAbuseAccountVideo> + +// ############################################################################ + +// Format for API or AP object + +export type MAbuseAdminFormattable = + MAbuse & + Use<'ReporterAccount', MAccountFormattable> & + Use<'FlaggedAccount', MAccountFormattable> & + Use<'VideoAbuse', MVideoAbuseFormattable> & + Use<'VideoCommentAbuse', MCommentAbuseFormattable> + +export type MAbuseUserFormattable = + MAbuse & + Use<'FlaggedAccount', MAccountFormattable> & + Use<'VideoAbuse', MVideoAbuseFormattable> & + Use<'VideoCommentAbuse', MCommentAbuseFormattable> + +export type MAbuseAP = + MAbuse & + Pick & + Use<'ReporterAccount', MAccountUrl> & + Use<'FlaggedAccount', MAccountUrl> & + Use<'VideoAbuse', MVideoAbuseVideo> & + Use<'VideoCommentAbuse', MCommentAbuseAccount> diff --git a/server/types/models/abuse/index.ts b/server/types/models/abuse/index.ts new file mode 100644 index 000000000..1ed91b249 --- /dev/null +++ b/server/types/models/abuse/index.ts @@ -0,0 +1,2 @@ +export * from './abuse' +export * from './abuse-message' diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts index 9513acad8..984841291 100644 --- a/server/types/models/account/account.ts +++ b/server/types/models/account/account.ts @@ -1,7 +1,5 @@ import { FunctionProperties, PickWith } from '@shared/core-utils' import { AccountModel } from '../../../models/account/account' -import { MChannelDefault } from '../video/video-channels' -import { MAccountBlocklistId } from './account-blocklist' import { MActor, MActorAPAccount, @@ -15,7 +13,9 @@ import { MActorSummary, MActorSummaryFormattable, MActorUrl -} from './actor' +} from '../actor' +import { MChannelDefault } from '../video/video-channels' +import { MAccountBlocklistId } from './account-blocklist' type Use = PickWith diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/account/actor-follow.ts deleted file mode 100644 index 8e19c6140..000000000 --- a/server/types/models/account/actor-follow.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { PickWith } from '@shared/core-utils' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' -import { - MActor, - MActorChannelAccountActor, - MActorDefault, - MActorDefaultAccountChannel, - MActorDefaultChannelId, - MActorFormattable, - MActorHost, - MActorUsername -} from './actor' - -type Use = PickWith - -// ############################################################################ - -export type MActorFollow = Omit - -// ############################################################################ - -export type MActorFollowFollowingHost = - MActorFollow & - Use<'ActorFollowing', MActorUsername & MActorHost> - -// ############################################################################ - -// With actors or actors default - -export type MActorFollowActors = - MActorFollow & - Use<'ActorFollower', MActor> & - Use<'ActorFollowing', MActor> - -export type MActorFollowActorsDefault = - MActorFollow & - Use<'ActorFollower', MActorDefault> & - Use<'ActorFollowing', MActorDefault> - -export type MActorFollowFull = - MActorFollow & - Use<'ActorFollower', MActorDefaultAccountChannel> & - Use<'ActorFollowing', MActorDefaultAccountChannel> - -// ############################################################################ - -// For subscriptions - -export type MActorFollowActorsDefaultSubscription = - MActorFollow & - Use<'ActorFollower', MActorDefault> & - Use<'ActorFollowing', MActorDefaultChannelId> - -export type MActorFollowSubscriptions = - MActorFollow & - Use<'ActorFollowing', MActorChannelAccountActor> - -// ############################################################################ - -// Format for API or AP object - -export type MActorFollowFormattable = - Pick & - Use<'ActorFollower', MActorFormattable> & - Use<'ActorFollowing', MActorFormattable> diff --git a/server/types/models/account/actor-image.ts b/server/types/models/account/actor-image.ts deleted file mode 100644 index e59f8b141..000000000 --- a/server/types/models/account/actor-image.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ActorImageModel } from '../../../models/account/actor-image' -import { FunctionProperties } from '@shared/core-utils' - -export type MActorImage = ActorImageModel - -// ############################################################################ - -// Format for API or AP object - -export type MActorImageFormattable = - FunctionProperties & - Pick diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts deleted file mode 100644 index 0b620872e..000000000 --- a/server/types/models/account/actor.ts +++ /dev/null @@ -1,166 +0,0 @@ - -import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' -import { ActorModel } from '../../../models/activitypub/actor' -import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' -import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' -import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' -import { MActorImage, MActorImageFormattable } from './actor-image' - -type Use = PickWith -type UseOpt = PickWithOpt - -// ############################################################################ - -export type MActor = Omit - -// ############################################################################ - -export type MActorUrl = Pick -export type MActorId = Pick -export type MActorUsername = Pick - -export type MActorFollowersUrl = Pick -export type MActorAudience = MActorUrl & MActorFollowersUrl -export type MActorWithInboxes = Pick -export type MActorSignature = MActorAccountChannelId - -export type MActorLight = Omit - -// ############################################################################ - -// Some association attributes - -export type MActorHost = Use<'Server', MServerHost> -export type MActorRedundancyAllowedOpt = PickWithOpt - -export type MActorDefaultLight = - MActorLight & - Use<'Server', MServerHost> & - Use<'Avatar', MActorImage> - -export type MActorAccountId = - MActor & - Use<'Account', MAccountId> -export type MActorAccountIdActor = - MActor & - Use<'Account', MAccountIdActor> - -export type MActorChannelId = - MActor & - Use<'VideoChannel', MChannelId> -export type MActorChannelIdActor = - MActor & - Use<'VideoChannel', MChannelIdActor> - -export type MActorAccountChannelId = MActorAccountId & MActorChannelId -export type MActorAccountChannelIdActor = MActorAccountIdActor & MActorChannelIdActor - -// ############################################################################ - -// Include raw account/channel/server - -export type MActorAccount = - MActor & - Use<'Account', MAccount> - -export type MActorChannel = - MActor & - Use<'VideoChannel', MChannel> - -export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel - -export type MActorServer = - MActor & - Use<'Server', MServer> - -// ############################################################################ - -// Complex actor associations - -export type MActorImages = - MActor & - Use<'Avatar', MActorImage> & - UseOpt<'Banner', MActorImage> - -export type MActorDefault = - MActor & - Use<'Server', MServer> & - Use<'Avatar', MActorImage> - -export type MActorDefaultChannelId = - MActorDefault & - Use<'VideoChannel', MChannelId> - -export type MActorDefaultBanner = - MActor & - Use<'Server', MServer> & - Use<'Avatar', MActorImage> & - Use<'Banner', MActorImage> - -// Actor with channel that is associated to an account and its actor -// Actor -> VideoChannel -> Account -> Actor -export type MActorChannelAccountActor = - MActor & - Use<'VideoChannel', MChannelAccountActor> - -export type MActorFull = - MActor & - Use<'Server', MServer> & - Use<'Avatar', MActorImage> & - Use<'Banner', MActorImage> & - Use<'Account', MAccount> & - Use<'VideoChannel', MChannelAccountActor> - -// Same than ActorFull, but the account and the channel have their actor -export type MActorFullActor = - MActor & - Use<'Server', MServer> & - Use<'Avatar', MActorImage> & - Use<'Banner', MActorImage> & - Use<'Account', MAccountDefault> & - Use<'VideoChannel', MChannelAccountDefault> - -// ############################################################################ - -// API - -export type MActorSummary = - FunctionProperties & - Pick & - Use<'Server', MServerHost> & - Use<'Avatar', MActorImage> - -export type MActorSummaryBlocks = - MActorSummary & - Use<'Server', MServerHostBlocks> - -export type MActorAPI = - Omit - -// ############################################################################ - -// Format for API or AP object - -export type MActorSummaryFormattable = - FunctionProperties & - Pick & - Use<'Server', MServerHost> & - Use<'Avatar', MActorImageFormattable> - -export type MActorFormattable = - MActorSummaryFormattable & - Pick & - Use<'Server', MServerHost & Partial>> & - UseOpt<'Banner', MActorImageFormattable> - -type MActorAPBase = - MActor & - Use<'Avatar', MActorImage> - -export type MActorAPAccount = - MActorAPBase - -export type MActorAPChannel = - MActorAPBase & - Use<'Banner', MActorImage> diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts index e3fc00f94..dab2eea7e 100644 --- a/server/types/models/account/index.ts +++ b/server/types/models/account/index.ts @@ -1,5 +1,2 @@ export * from './account' export * from './account-blocklist' -export * from './actor-follow' -export * from './actor-image' -export * from './actor' diff --git a/server/types/models/actor/actor-follow.ts b/server/types/models/actor/actor-follow.ts new file mode 100644 index 000000000..98a6ca8a5 --- /dev/null +++ b/server/types/models/actor/actor-follow.ts @@ -0,0 +1,65 @@ +import { PickWith } from '@shared/core-utils' +import { ActorFollowModel } from '../../../models/actor/actor-follow' +import { + MActor, + MActorChannelAccountActor, + MActorDefault, + MActorDefaultAccountChannel, + MActorDefaultChannelId, + MActorFormattable, + MActorHost, + MActorUsername +} from './actor' + +type Use = PickWith + +// ############################################################################ + +export type MActorFollow = Omit + +// ############################################################################ + +export type MActorFollowFollowingHost = + MActorFollow & + Use<'ActorFollowing', MActorUsername & MActorHost> + +// ############################################################################ + +// With actors or actors default + +export type MActorFollowActors = + MActorFollow & + Use<'ActorFollower', MActor> & + Use<'ActorFollowing', MActor> + +export type MActorFollowActorsDefault = + MActorFollow & + Use<'ActorFollower', MActorDefault> & + Use<'ActorFollowing', MActorDefault> + +export type MActorFollowFull = + MActorFollow & + Use<'ActorFollower', MActorDefaultAccountChannel> & + Use<'ActorFollowing', MActorDefaultAccountChannel> + +// ############################################################################ + +// For subscriptions + +export type MActorFollowActorsDefaultSubscription = + MActorFollow & + Use<'ActorFollower', MActorDefault> & + Use<'ActorFollowing', MActorDefaultChannelId> + +export type MActorFollowSubscriptions = + MActorFollow & + Use<'ActorFollowing', MActorChannelAccountActor> + +// ############################################################################ + +// Format for API or AP object + +export type MActorFollowFormattable = + Pick & + Use<'ActorFollower', MActorFormattable> & + Use<'ActorFollowing', MActorFormattable> diff --git a/server/types/models/actor/actor-image.ts b/server/types/models/actor/actor-image.ts new file mode 100644 index 000000000..89adb01ae --- /dev/null +++ b/server/types/models/actor/actor-image.ts @@ -0,0 +1,12 @@ +import { FunctionProperties } from '@shared/core-utils' +import { ActorImageModel } from '../../../models/actor/actor-image' + +export type MActorImage = ActorImageModel + +// ############################################################################ + +// Format for API or AP object + +export type MActorImageFormattable = + FunctionProperties & + Pick diff --git a/server/types/models/actor/actor.ts b/server/types/models/actor/actor.ts new file mode 100644 index 000000000..b3a70cbce --- /dev/null +++ b/server/types/models/actor/actor.ts @@ -0,0 +1,165 @@ +import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' +import { ActorModel } from '../../../models/actor/actor' +import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from '../account' +import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' +import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' +import { MActorImage, MActorImageFormattable } from './actor-image' + +type Use = PickWith +type UseOpt = PickWithOpt + +// ############################################################################ + +export type MActor = Omit + +// ############################################################################ + +export type MActorUrl = Pick +export type MActorId = Pick +export type MActorUsername = Pick + +export type MActorFollowersUrl = Pick +export type MActorAudience = MActorUrl & MActorFollowersUrl +export type MActorWithInboxes = Pick +export type MActorSignature = MActorAccountChannelId + +export type MActorLight = Omit + +// ############################################################################ + +// Some association attributes + +export type MActorHost = Use<'Server', MServerHost> +export type MActorRedundancyAllowedOpt = PickWithOpt + +export type MActorDefaultLight = + MActorLight & + Use<'Server', MServerHost> & + Use<'Avatar', MActorImage> + +export type MActorAccountId = + MActor & + Use<'Account', MAccountId> +export type MActorAccountIdActor = + MActor & + Use<'Account', MAccountIdActor> + +export type MActorChannelId = + MActor & + Use<'VideoChannel', MChannelId> +export type MActorChannelIdActor = + MActor & + Use<'VideoChannel', MChannelIdActor> + +export type MActorAccountChannelId = MActorAccountId & MActorChannelId +export type MActorAccountChannelIdActor = MActorAccountIdActor & MActorChannelIdActor + +// ############################################################################ + +// Include raw account/channel/server + +export type MActorAccount = + MActor & + Use<'Account', MAccount> + +export type MActorChannel = + MActor & + Use<'VideoChannel', MChannel> + +export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel + +export type MActorServer = + MActor & + Use<'Server', MServer> + +// ############################################################################ + +// Complex actor associations + +export type MActorImages = + MActor & + Use<'Avatar', MActorImage> & + UseOpt<'Banner', MActorImage> + +export type MActorDefault = + MActor & + Use<'Server', MServer> & + Use<'Avatar', MActorImage> + +export type MActorDefaultChannelId = + MActorDefault & + Use<'VideoChannel', MChannelId> + +export type MActorDefaultBanner = + MActor & + Use<'Server', MServer> & + Use<'Avatar', MActorImage> & + Use<'Banner', MActorImage> + +// Actor with channel that is associated to an account and its actor +// Actor -> VideoChannel -> Account -> Actor +export type MActorChannelAccountActor = + MActor & + Use<'VideoChannel', MChannelAccountActor> + +export type MActorFull = + MActor & + Use<'Server', MServer> & + Use<'Avatar', MActorImage> & + Use<'Banner', MActorImage> & + Use<'Account', MAccount> & + Use<'VideoChannel', MChannelAccountActor> + +// Same than ActorFull, but the account and the channel have their actor +export type MActorFullActor = + MActor & + Use<'Server', MServer> & + Use<'Avatar', MActorImage> & + Use<'Banner', MActorImage> & + Use<'Account', MAccountDefault> & + Use<'VideoChannel', MChannelAccountDefault> + +// ############################################################################ + +// API + +export type MActorSummary = + FunctionProperties & + Pick & + Use<'Server', MServerHost> & + Use<'Avatar', MActorImage> + +export type MActorSummaryBlocks = + MActorSummary & + Use<'Server', MServerHostBlocks> + +export type MActorAPI = + Omit + +// ############################################################################ + +// Format for API or AP object + +export type MActorSummaryFormattable = + FunctionProperties & + Pick & + Use<'Server', MServerHost> & + Use<'Avatar', MActorImageFormattable> + +export type MActorFormattable = + MActorSummaryFormattable & + Pick & + Use<'Server', MServerHost & Partial>> & + UseOpt<'Banner', MActorImageFormattable> + +type MActorAPBase = + MActor & + Use<'Avatar', MActorImage> + +export type MActorAPAccount = + MActorAPBase + +export type MActorAPChannel = + MActorAPBase & + Use<'Banner', MActorImage> diff --git a/server/types/models/actor/index.ts b/server/types/models/actor/index.ts new file mode 100644 index 000000000..b27815255 --- /dev/null +++ b/server/types/models/actor/index.ts @@ -0,0 +1,3 @@ +export * from './actor-follow' +export * from './actor-image' +export * from './actor' diff --git a/server/types/models/index.ts b/server/types/models/index.ts index b4fdb1ff3..704cb9844 100644 --- a/server/types/models/index.ts +++ b/server/types/models/index.ts @@ -1,6 +1,7 @@ +export * from './abuse' export * from './account' +export * from './actor' export * from './application' -export * from './moderation' export * from './oauth' export * from './server' export * from './user' diff --git a/server/types/models/moderation/abuse-message.ts b/server/types/models/moderation/abuse-message.ts deleted file mode 100644 index 565eca706..000000000 --- a/server/types/models/moderation/abuse-message.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AbuseMessageModel } from '@server/models/abuse/abuse-message' -import { PickWith } from '@shared/core-utils' -import { AbuseModel } from '../../../models/abuse/abuse' -import { MAccountFormattable } from '../account' - -type Use = PickWith - -// ############################################################################ - -export type MAbuseMessage = Omit - -export type MAbuseMessageId = Pick - -// ############################################################################ - -// Format for API - -export type MAbuseMessageFormattable = - MAbuseMessage & - Use<'Account', MAccountFormattable> diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts deleted file mode 100644 index 6fd83684c..000000000 --- a/server/types/models/moderation/abuse.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { VideoAbuseModel } from '@server/models/abuse/video-abuse' -import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' -import { VideoCommentModel } from '@server/models/video/video-comment' -import { PickWith } from '@shared/core-utils' -import { AbuseModel } from '../../../models/abuse/abuse' -import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account' -import { MComment, MCommentOwner, MCommentUrl, MCommentVideo, MVideoUrl } from '../video' -import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video' - -type Use = PickWith -type UseVideoAbuse = PickWith -type UseCommentAbuse = PickWith - -// ############################################################################ - -export type MAbuse = Omit - -export type MVideoAbuse = Omit - -export type MCommentAbuse = Omit - -export type MAbuseReporter = - MAbuse & - Use<'ReporterAccount', MAccountDefault> - -// ############################################################################ - -export type MVideoAbuseVideo = - MVideoAbuse & - UseVideoAbuse<'Video', MVideo> - -export type MVideoAbuseVideoUrl = - MVideoAbuse & - UseVideoAbuse<'Video', MVideoUrl> - -export type MVideoAbuseVideoFull = - MVideoAbuse & - UseVideoAbuse<'Video', Omit> - -export type MVideoAbuseFormattable = - MVideoAbuse & - UseVideoAbuse<'Video', Pick> - -// ############################################################################ - -export type MCommentAbuseAccount = - MCommentAbuse & - UseCommentAbuse<'VideoComment', MCommentOwner> - -export type MCommentAbuseAccountVideo = - MCommentAbuse & - UseCommentAbuse<'VideoComment', MCommentOwner & PickWith> - -export type MCommentAbuseUrl = - MCommentAbuse & - UseCommentAbuse<'VideoComment', MCommentUrl> - -export type MCommentAbuseFormattable = - MCommentAbuse & - UseCommentAbuse<'VideoComment', MComment & PickWith>> - -// ############################################################################ - -export type MAbuseId = Pick - -export type MAbuseVideo = - MAbuse & - Pick & - Use<'VideoAbuse', MVideoAbuseVideo> - -export type MAbuseUrl = - MAbuse & - Use<'VideoAbuse', MVideoAbuseVideoUrl> & - Use<'VideoCommentAbuse', MCommentAbuseUrl> - -export type MAbuseAccountVideo = - MAbuse & - Pick & - Use<'VideoAbuse', MVideoAbuseVideoFull> & - Use<'ReporterAccount', MAccountDefault> - -export type MAbuseFull = - MAbuse & - Pick & - Use<'ReporterAccount', MAccountLight> & - Use<'FlaggedAccount', MAccountLight> & - Use<'VideoAbuse', MVideoAbuseVideoFull> & - Use<'VideoCommentAbuse', MCommentAbuseAccountVideo> - -// ############################################################################ - -// Format for API or AP object - -export type MAbuseAdminFormattable = - MAbuse & - Use<'ReporterAccount', MAccountFormattable> & - Use<'FlaggedAccount', MAccountFormattable> & - Use<'VideoAbuse', MVideoAbuseFormattable> & - Use<'VideoCommentAbuse', MCommentAbuseFormattable> - -export type MAbuseUserFormattable = - MAbuse & - Use<'FlaggedAccount', MAccountFormattable> & - Use<'VideoAbuse', MVideoAbuseFormattable> & - Use<'VideoCommentAbuse', MCommentAbuseFormattable> - -export type MAbuseAP = - MAbuse & - Pick & - Use<'ReporterAccount', MAccountUrl> & - Use<'FlaggedAccount', MAccountUrl> & - Use<'VideoAbuse', MVideoAbuseVideo> & - Use<'VideoCommentAbuse', MCommentAbuseAccount> diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts deleted file mode 100644 index 1ed91b249..000000000 --- a/server/types/models/moderation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './abuse' -export * from './abuse-message' diff --git a/server/types/models/user/user-notification-setting.ts b/server/types/models/user/user-notification-setting.ts index c674add1b..d1db645e7 100644 --- a/server/types/models/user/user-notification-setting.ts +++ b/server/types/models/user/user-notification-setting.ts @@ -1,4 +1,4 @@ -import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting' +import { UserNotificationSettingModel } from '@server/models/user/user-notification-setting' export type MNotificationSetting = Omit diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 7ebb0485d..918614dd1 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts @@ -2,13 +2,13 @@ import { VideoAbuseModel } from '@server/models/abuse/video-abuse' import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' import { ApplicationModel } from '@server/models/application/application' import { PluginModel } from '@server/models/server/plugin' +import { UserNotificationModel } from '@server/models/user/user-notification' import { PickWith, PickWithOpt } from '@shared/core-utils' import { AbuseModel } from '../../../models/abuse/abuse' import { AccountModel } from '../../../models/account/account' -import { ActorImageModel } from '../../../models/account/actor-image' -import { UserNotificationModel } from '../../../models/account/user-notification' -import { ActorModel } from '../../../models/activitypub/actor' -import { ActorFollowModel } from '../../../models/activitypub/actor-follow' +import { ActorModel } from '../../../models/actor/actor' +import { ActorFollowModel } from '../../../models/actor/actor-follow' +import { ActorImageModel } from '../../../models/actor/actor-image' import { ServerModel } from '../../../models/server/server' import { VideoModel } from '../../../models/video/video' import { VideoBlacklistModel } from '../../../models/video/video-blacklist' diff --git a/server/types/models/user/user-video-history.ts b/server/types/models/user/user-video-history.ts index 62673ab1b..34e2930e7 100644 --- a/server/types/models/user/user-video-history.ts +++ b/server/types/models/user/user-video-history.ts @@ -1,4 +1,4 @@ -import { UserVideoHistoryModel } from '../../../models/account/user-video-history' +import { UserVideoHistoryModel } from '../../../models/user/user-video-history' export type MUserVideoHistory = Omit diff --git a/server/types/models/user/user.ts b/server/types/models/user/user.ts index fa7de9c52..f79220e11 100644 --- a/server/types/models/user/user.ts +++ b/server/types/models/user/user.ts @@ -1,7 +1,7 @@ import { AccountModel } from '@server/models/account/account' +import { UserModel } from '@server/models/user/user' import { MVideoPlaylist } from '@server/types/models' import { PickWith, PickWithOpt } from '@shared/core-utils' -import { UserModel } from '../../../models/account/user' import { MAccount, MAccountDefault, diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts index f577807ca..c147567d9 100644 --- a/server/types/models/video/video-channels.ts +++ b/server/types/models/video/video-channels.ts @@ -9,7 +9,9 @@ import { MAccountSummaryBlocks, MAccountSummaryFormattable, MAccountUrl, - MAccountUserId, + MAccountUserId +} from '../account' +import { MActor, MActorAccountChannelId, MActorAPChannel, @@ -23,7 +25,7 @@ import { MActorSummary, MActorSummaryFormattable, MActorUrl -} from '../account' +} from '../actor' import { MVideo } from './video' type Use = PickWith diff --git a/server/types/models/video/video-share.ts b/server/types/models/video/video-share.ts index b7a783bb6..78f44e58c 100644 --- a/server/types/models/video/video-share.ts +++ b/server/types/models/video/video-share.ts @@ -1,6 +1,6 @@ -import { VideoShareModel } from '../../../models/video/video-share' import { PickWith } from '@shared/core-utils' -import { MActorDefault } from '../account' +import { VideoShareModel } from '../../../models/video/video-share' +import { MActorDefault } from '../actor' import { MVideo } from './video' type Use = PickWith diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 2432b7ac4..8774bcd8c 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts @@ -1,6 +1,6 @@ -import { Router, Response } from 'express' +import { Response, Router } from 'express' import { Logger } from 'winston' -import { ActorModel } from '@server/models/activitypub/actor' +import { ActorModel } from '@server/models/actor/actor' import { PluginPlaylistPrivacyManager, PluginSettingsManager, -- cgit v1.2.3