Add audit logs in various modules
authorAurélien Bertron <aurelienbertron@gmail.com>
Tue, 31 Jul 2018 12:04:26 +0000 (14:04 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 31 Jul 2018 13:40:29 +0000 (15:40 +0200)
- Videos
- Videos comments
- Users
- Videos channels
- Videos abuses
- Custom config

server/controllers/api/config.ts
server/controllers/api/users.ts
server/controllers/api/video-channel.ts
server/controllers/api/videos/abuse.ts
server/controllers/api/videos/comment.ts
server/controllers/api/videos/index.ts
server/helpers/audit-logger.ts
server/lib/user.ts
server/models/activitypub/actor.ts

index 9c1b2818c15330fda89457c5e156680df7421baa..411b135394f83fd0a4f4e21434b491e3273514ca 100644 (file)
@@ -9,10 +9,13 @@ import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
 import { customConfigUpdateValidator } from '../../middlewares/validators/config'
 import { ClientHtml } from '../../lib/client-html'
+import { CustomConfigAuditView, auditLoggerFactory } from '../../helpers/audit-logger'
 
 const packageJSON = require('../../../../package.json')
 const configRouter = express.Router()
 
+const auditLogger = auditLoggerFactory('config')
+
 configRouter.get('/about', getAbout)
 configRouter.get('/',
   asyncMiddleware(getConfig)
@@ -119,6 +122,11 @@ async function getCustomConfig (req: express.Request, res: express.Response, nex
 async function deleteCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
   await unlinkPromise(CONFIG.CUSTOM_FILE)
 
+  auditLogger.delete(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new CustomConfigAuditView(customConfig())
+  )
+
   reloadConfig()
   ClientHtml.invalidCache()
 
@@ -129,6 +137,7 @@ async function deleteCustomConfig (req: express.Request, res: express.Response,
 
 async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
   const toUpdate: CustomConfig = req.body
+  const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
 
   // Force number conversion
   toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10)
@@ -150,6 +159,13 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
   ClientHtml.invalidCache()
 
   const data = customConfig()
+
+  auditLogger.update(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new CustomConfigAuditView(data),
+    oldCustomConfigAuditKeys
+  )
+
   return res.json(data).end()
 }
 
index c80f27a23c53b19b4dc96c91cc89ece3c7026873..dbe736bff9a6cb57eba32e02ad5846ea826ba8cc 100644 (file)
@@ -39,6 +39,9 @@ import { createReqFiles } from '../../helpers/express-utils'
 import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.model'
 import { updateAvatarValidator } from '../../middlewares/validators/avatar'
 import { updateActorAvatarFile } from '../../lib/avatar'
+import { auditLoggerFactory, UserAuditView } from '../../helpers/audit-logger'
+
+const auditLogger = auditLoggerFactory('users')
 
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
 const loginRateLimiter = new RateLimit({
@@ -189,6 +192,7 @@ async function createUser (req: express.Request, res: express.Response) {
 
   const { user, account } = await createUserAccountAndChannel(userToCreate)
 
+  auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON()))
   logger.info('User %s with its channel and account created.', body.username)
 
   return res.json({
@@ -205,7 +209,7 @@ async function createUser (req: express.Request, res: express.Response) {
 async function registerUser (req: express.Request, res: express.Response) {
   const body: UserCreate = req.body
 
-  const user = new UserModel({
+  const userToCreate = new UserModel({
     username: body.username,
     password: body.password,
     email: body.email,
@@ -215,8 +219,9 @@ async function registerUser (req: express.Request, res: express.Response) {
     videoQuota: CONFIG.USER.VIDEO_QUOTA
   })
 
-  await createUserAccountAndChannel(user)
+  const { user } = await createUserAccountAndChannel(userToCreate)
 
+  auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
   logger.info('User %s with its channel and account registered.', body.username)
 
   return res.type('json').status(204).end()
@@ -269,6 +274,8 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
 
   await user.destroy()
 
+  auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON()))
+
   return res.sendStatus(204)
 }
 
@@ -276,6 +283,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
   const body: UserUpdateMe = req.body
 
   const user: UserModel = res.locals.oauth.token.user
+  const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
 
   if (body.password !== undefined) user.password = body.password
   if (body.email !== undefined) user.email = body.email
@@ -290,6 +298,12 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
     await user.Account.save({ transaction: t })
 
     await sendUpdateActor(user.Account, t)
+
+    auditLogger.update(
+      res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+      new UserAuditView(user.toFormattedJSON()),
+      oldUserAuditView
+    )
   })
 
   return res.sendStatus(204)
@@ -297,10 +311,18 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
 
 async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
   const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
-  const account = res.locals.oauth.token.user.Account
+  const user: UserModel = res.locals.oauth.token.user
+  const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
+  const account = user.Account
 
   const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account)
 
+  auditLogger.update(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new UserAuditView(user.toFormattedJSON()),
+    oldUserAuditView
+  )
+
   return res
     .json({
       avatar: avatar.toFormattedJSON()
@@ -310,20 +332,27 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
 
 async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
   const body: UserUpdate = req.body
-  const user = res.locals.user as UserModel
-  const roleChanged = body.role !== undefined && body.role !== user.role
+  const userToUpdate = res.locals.user as UserModel
+  const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
+  const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
 
-  if (body.email !== undefined) user.email = body.email
-  if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
-  if (body.role !== undefined) user.role = body.role
+  if (body.email !== undefined) userToUpdate.email = body.email
+  if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
+  if (body.role !== undefined) userToUpdate.role = body.role
 
-  await user.save()
+  const user = await userToUpdate.save()
 
   // Destroy user token to refresh rights
   if (roleChanged) {
-    await OAuthTokenModel.deleteUserToken(user.id)
+    await OAuthTokenModel.deleteUserToken(userToUpdate.id)
   }
 
+  auditLogger.update(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new UserAuditView(user.toFormattedJSON()),
+    oldUserAuditView
+  )
+
   // Don't need to send this update to followers, these attributes are not propagated
 
   return res.sendStatus(204)
index 0488ba8f5911d1862aa3ff190940b4b90effa639..3a444547b66666c96437c47e884a185760905437 100644 (file)
@@ -27,7 +27,9 @@ import { logger } from '../../helpers/logger'
 import { VideoModel } from '../../models/video/video'
 import { updateAvatarValidator } from '../../middlewares/validators/avatar'
 import { updateActorAvatarFile } from '../../lib/avatar'
+import { auditLoggerFactory, VideoChannelAuditView } from '../../helpers/audit-logger'
 
+const auditLogger = auditLoggerFactory('channels')
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
 
 const videoChannelRouter = express.Router()
@@ -99,10 +101,17 @@ async function listVideoChannels (req: express.Request, res: express.Response, n
 
 async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
   const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
-  const videoChannel = res.locals.videoChannel
+  const videoChannel = res.locals.videoChannel as VideoChannelModel
+  const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
 
   const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel.Actor, videoChannel)
 
+  auditLogger.update(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new VideoChannelAuditView(videoChannel.toFormattedJSON()),
+    oldVideoChannelAuditKeys
+  )
+
   return res
     .json({
       avatar: avatar.toFormattedJSON()
@@ -121,6 +130,10 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
   setAsyncActorKeys(videoChannelCreated.Actor)
     .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err }))
 
+  auditLogger.create(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new VideoChannelAuditView(videoChannelCreated.toFormattedJSON())
+  )
   logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid)
 
   return res.json({
@@ -134,6 +147,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
 async function updateVideoChannel (req: express.Request, res: express.Response) {
   const videoChannelInstance = res.locals.videoChannel as VideoChannelModel
   const videoChannelFieldsSave = videoChannelInstance.toJSON()
+  const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
   const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
 
   try {
@@ -148,9 +162,14 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
 
       const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
       await sendUpdateActor(videoChannelInstanceUpdated, t)
-    })
 
-    logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
+      auditLogger.update(
+        res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+        new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
+        oldVideoChannelAuditKeys
+      )
+      logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
+    })
   } catch (err) {
     logger.debug('Cannot update the video channel.', { err })
 
@@ -171,6 +190,10 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
   await sequelizeTypescript.transaction(async t => {
     await videoChannelInstance.destroy({ transaction: t })
 
+    auditLogger.delete(
+      res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+      new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
+    )
     logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid)
   })
 
index 3413ae894d40fcf32b170a18e5a4e569533f7a14..7782fc6390cb3aa187d11ba23b5f5cc3e632548a 100644 (file)
@@ -18,7 +18,9 @@ import {
 import { AccountModel } from '../../../models/account/account'
 import { VideoModel } from '../../../models/video/video'
 import { VideoAbuseModel } from '../../../models/video/video-abuse'
+import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
 
+const auditLogger = auditLoggerFactory('abuse')
 const abuseVideoRouter = express.Router()
 
 abuseVideoRouter.get('/abuse',
@@ -64,14 +66,16 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
   await sequelizeTypescript.transaction(async t => {
     const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
     videoAbuseInstance.Video = videoInstance
+    videoAbuseInstance.Account = reporterAccount
 
     // We send the video abuse to the origin server
     if (videoInstance.isOwned() === false) {
       await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
     }
-  })
 
-  logger.info('Abuse report for video %s created.', videoInstance.name)
+    auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON()))
+    logger.info('Abuse report for video %s created.', videoInstance.name)
+  })
 
   return res.type('json').status(204).end()
 }
index bbeb0d55786c0a53f22bbadbfca9cfec249ff142..e35247829b249c7ae62899908b0373a31a8c6967 100644 (file)
@@ -23,7 +23,9 @@ import {
 } from '../../../middlewares/validators/video-comments'
 import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
+import { auditLoggerFactory, CommentAuditView } from '../../../helpers/audit-logger'
 
+const auditLogger = auditLoggerFactory('comments')
 const videoCommentRouter = express.Router()
 
 videoCommentRouter.get('/:videoId/comment-threads',
@@ -107,6 +109,8 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
     }, t)
   })
 
+  auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new CommentAuditView(comment.toFormattedJSON()))
+
   return res.json({
     comment: comment.toFormattedJSON()
   }).end()
@@ -124,6 +128,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
     }, t)
   })
 
+  auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new CommentAuditView(comment.toFormattedJSON()))
+
   return res.json({
     comment: comment.toFormattedJSON()
   }).end()
@@ -136,6 +142,10 @@ async function removeVideoComment (req: express.Request, res: express.Response)
     await videoCommentInstance.destroy({ transaction: t })
   })
 
+  auditLogger.delete(
+    res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+    new CommentAuditView(videoCommentInstance.toFormattedJSON())
+  )
   logger.info('Video comment %d deleted.', videoCommentInstance.id)
 
   return res.type('json').status(204).end()
index 101183eabd9ac65e2a85f944beb4639ac85d4f0d..e396ee6be7b83a4462b90ce0af63bf24ac4aca90 100644 (file)
@@ -5,6 +5,7 @@ import { renamePromise } from '../../../helpers/core-utils'
 import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
 import { processImage } from '../../../helpers/image-utils'
 import { logger } from '../../../helpers/logger'
+import { auditLoggerFactory, VideoAuditView } from '../../../helpers/audit-logger'
 import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
 import {
   CONFIG,
@@ -54,6 +55,7 @@ import { createReqFiles, buildNSFWFilter } from '../../../helpers/express-utils'
 import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 import { videoCaptionsRouter } from './captions'
 
+const auditLogger = auditLoggerFactory('videos')
 const videosRouter = express.Router()
 
 const reqVideoFileAdd = createReqFiles(
@@ -247,6 +249,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 
     await federateVideoIfNeeded(video, true, t)
 
+    auditLogger.create(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
     logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
 
     return videoCreated
@@ -273,6 +276,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 async function updateVideo (req: express.Request, res: express.Response) {
   const videoInstance: VideoModel = res.locals.video
   const videoFieldsSave = videoInstance.toJSON()
+  const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
   const videoInfoToUpdate: VideoUpdate = req.body
   const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
 
@@ -344,9 +348,14 @@ async function updateVideo (req: express.Request, res: express.Response) {
 
       const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
       await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
-    })
 
-    logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
+      auditLogger.update(
+        res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+        new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
+        oldVideoAuditView
+      )
+      logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
+    })
   } catch (err) {
     // Force fields we want to update
     // If the transaction is retried, sequelize will think the object has not changed
@@ -423,6 +432,7 @@ async function removeVideo (req: express.Request, res: express.Response) {
     await videoInstance.destroy({ transaction: t })
   })
 
+  auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
   logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid)
 
   return res.type('json').status(204).end()
index 4b237316fa017c3a8ec5879c6bcb459386094875..f6eea7d9046707b159e4b0396ac31bf46789a34d 100644 (file)
@@ -5,7 +5,9 @@ import * as flatten from 'flat'
 import * as winston from 'winston'
 import { CONFIG } from '../initializers'
 import { jsonLoggerFormat, labelFormatter } from './logger'
-import { VideoDetails } from '../../shared'
+import { VideoDetails, User, VideoChannel, VideoAbuse } from '../../shared'
+import { VideoComment } from '../../shared/models/videos/video-comment.model'
+import { CustomConfig } from '../../shared/models/server/custom-config.model'
 
 enum AUDIT_TYPE {
   CREATE = 'create',
@@ -111,13 +113,143 @@ const videoKeysToKeep = [
   'support',
   'commentsEnabled'
 ]
-class VideoAuditView extends AuditEntity {
+class VideoAuditView extends EntityAuditView {
   constructor (private video: VideoDetails) {
     super(videoKeysToKeep, 'video', video)
   }
 }
 
+const commentKeysToKeep = [
+  'id',
+  'text',
+  'threadId',
+  'inReplyToCommentId',
+  'videoId',
+  'createdAt',
+  'updatedAt',
+  'totalReplies',
+  'account-id',
+  'account-uuid',
+  'account-name'
+]
+class CommentAuditView extends EntityAuditView {
+  constructor (private comment: VideoComment) {
+    super(commentKeysToKeep, 'comment', comment)
+  }
+}
+
+const userKeysToKeep = [
+  'id',
+  'username',
+  'email',
+  'nsfwPolicy',
+  'autoPlayVideo',
+  'role',
+  'videoQuota',
+  'createdAt',
+  'account-id',
+  'account-uuid',
+  'account-name',
+  'account-followingCount',
+  'account-followersCount',
+  'account-createdAt',
+  'account-updatedAt',
+  'account-avatar-path',
+  'account-avatar-createdAt',
+  'account-avatar-updatedAt',
+  'account-displayName',
+  'account-description',
+  'videoChannels'
+]
+class UserAuditView extends EntityAuditView {
+  constructor (private user: User) {
+    super(userKeysToKeep, 'user', user)
+  }
+}
+
+const channelKeysToKeep = [
+  'id',
+  'uuid',
+  'name',
+  'followingCount',
+  'followersCount',
+  'createdAt',
+  'updatedAt',
+  'avatar-path',
+  'avatar-createdAt',
+  'avatar-updatedAt',
+  'displayName',
+  'description',
+  'support',
+  'isLocal',
+  'ownerAccount-id',
+  'ownerAccount-uuid',
+  'ownerAccount-name',
+  'ownerAccount-displayedName'
+]
+class VideoChannelAuditView extends EntityAuditView {
+  constructor (private channel: VideoChannel) {
+    super(channelKeysToKeep, 'channel', channel)
+  }
+}
+
+const videoAbuseKeysToKeep = [
+  'id',
+  'reason',
+  'reporterAccount',
+  'video-id',
+  'video-name',
+  'video-uuid',
+  'createdAt'
+]
+class VideoAbuseAuditView extends EntityAuditView {
+  constructor (private videoAbuse: VideoAbuse) {
+    super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
+  }
+}
+
+const customConfigKeysToKeep = [
+  'instance-name',
+  'instance-shortDescription',
+  'instance-description',
+  'instance-terms',
+  'instance-defaultClientRoute',
+  'instance-defaultNSFWPolicy',
+  'instance-customizations-javascript',
+  'instance-customizations-css',
+  'services-twitter-username',
+  'services-twitter-whitelisted',
+  'cache-previews-size',
+  'cache-captions-size',
+  'signup-enabled',
+  'signup-limit',
+  'admin-email',
+  'user-videoQuota',
+  'transcoding-enabled',
+  'transcoding-threads',
+  'transcoding-resolutions'
+]
+class CustomConfigAuditView extends EntityAuditView {
+  constructor (customConfig: CustomConfig) {
+    const infos: any = customConfig
+    const resolutionsDict = infos.transcoding.resolutions
+    const resolutionsArray = []
+    Object.entries(resolutionsDict).forEach(([resolution, isEnabled]) => {
+      if (isEnabled) {
+        resolutionsArray.push(resolution)
+      }
+    })
+    infos.transcoding.resolutions = resolutionsArray
+    super(customConfigKeysToKeep, 'config', infos)
+  }
+}
+
 export {
   auditLoggerFactory,
-  VideoAuditView
+  VideoChannelAuditView,
+  CommentAuditView,
+  UserAuditView,
+  VideoAuditView,
+  VideoAbuseAuditView,
+  CustomConfigAuditView
 }
index ac5f55260ea5b68e290b463cbcd072f13578ff30..e7a45f5aa72b4c96bcdeb6880facb43eb02c1dbb 100644 (file)
@@ -17,6 +17,7 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
 
     const userCreated = await userToCreate.save(userOptions)
     const accountCreated = await createLocalAccountWithoutKeys(userToCreate.username, userToCreate.id, null, t)
+    userCreated.Account = accountCreated
 
     const videoChannelDisplayName = `Default ${userCreated.username} channel`
     const videoChannelInfo = {
index 267032e2ab7e6ff9058a7025e34f432fe0a5c508..aeb69e7b4f59634e4d81729d581d22ad0718240a 100644 (file)
@@ -454,6 +454,10 @@ export class ActorModel extends Model<ActorModel> {
     return 'acct:' + this.preferredUsername + '@' + this.getHost()
   }
 
+  getIdentifier () {
+    return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
+  }
+
   getHost () {
     return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
   }