-import { join } from 'path'
+import { values } from 'lodash'
+import { extname } from 'path'
import * as Sequelize from 'sequelize'
import {
- AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table,
- UpdatedAt
+ AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes,
+ Table, UpdatedAt
} from 'sequelize-typescript'
+import { ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
-import { activityPubContextify } from '../../helpers'
+import { activityPubContextify } from '../../helpers/activitypub'
import {
- isActivityPubUrlValid,
- isActorFollowersCountValid,
- isActorFollowingCountValid, isActorPreferredUsernameValid,
- isActorPrivateKeyValid,
+ isActorFollowersCountValid, isActorFollowingCountValid, isActorPreferredUsernameValid, isActorPrivateKeyValid,
isActorPublicKeyValid
-} from '../../helpers/custom-validators/activitypub'
-import { isUserUsernameValid } from '../../helpers/custom-validators/users'
-import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
-import { AccountFollowModel } from '../account/account-follow'
+} from '../../helpers/custom-validators/activitypub/actor'
+import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
+import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
+import { AccountModel } from '../account/account'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server'
import { throwIfNotValid } from '../utils'
+import { VideoChannelModel } from '../video/video-channel'
+import { ActorFollowModel } from './actor-follow'
+enum ScopeNames {
+ FULL = 'FULL'
+}
+
+@DefaultScope({
+ include: [
+ {
+ model: () => ServerModel,
+ required: false
+ },
+ {
+ model: () => AvatarModel,
+ required: false
+ }
+ ]
+})
+@Scopes({
+ [ScopeNames.FULL]: {
+ include: [
+ {
+ model: () => AccountModel.unscoped(),
+ required: false
+ },
+ {
+ model: () => VideoChannelModel.unscoped(),
+ required: false
+ },
+ {
+ model: () => ServerModel,
+ required: false
+ },
+ {
+ model: () => AvatarModel,
+ required: false
+ }
+ ]
+ }
+})
@Table({
- tableName: 'actor'
+ tableName: 'actor',
+ indexes: [
+ {
+ fields: [ 'url' ]
+ },
+ {
+ fields: [ 'preferredUsername', 'serverId' ],
+ unique: true
+ },
+ {
+ fields: [ 'inboxUrl', 'sharedInboxUrl' ]
+ }
+ ]
})
export class ActorModel extends Model<ActorModel> {
+ @AllowNull(false)
+ @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
+ type: ActivityPubActorType
+
@AllowNull(false)
@Default(DataType.UUIDV4)
@IsUUID(4)
uuid: string
@AllowNull(false)
- @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name'))
+ @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
@Column
- name: string
+ preferredUsername: string
@AllowNull(false)
@Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
url: string
@AllowNull(true)
@Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
publicKey: string
@AllowNull(true)
@Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
privateKey: string
@AllowNull(false)
@AllowNull(false)
@Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
inboxUrl: string
@AllowNull(false)
@Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
outboxUrl: string
@AllowNull(false)
@Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
sharedInboxUrl: string
@AllowNull(false)
@Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
followersUrl: string
@AllowNull(false)
@Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.URL.max))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
followingUrl: string
@CreatedAt
foreignKey: {
allowNull: true
},
- onDelete: 'cascade'
+ onDelete: 'set null'
})
Avatar: AvatarModel
- @HasMany(() => AccountFollowModel, {
+ @HasMany(() => ActorFollowModel, {
foreignKey: {
- name: 'accountId',
+ name: 'actorId',
allowNull: false
},
onDelete: 'cascade'
})
- AccountFollowing: AccountFollowModel[]
+ AccountFollowing: ActorFollowModel[]
- @HasMany(() => AccountFollowModel, {
+ @HasMany(() => ActorFollowModel, {
foreignKey: {
- name: 'targetAccountId',
+ name: 'targetActorId',
allowNull: false
},
as: 'followers',
onDelete: 'cascade'
})
- AccountFollowers: AccountFollowModel[]
+ AccountFollowers: ActorFollowModel[]
@ForeignKey(() => ServerModel)
@Column
})
Server: ServerModel
+ @HasOne(() => AccountModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ Account: AccountModel
+
+ @HasOne(() => VideoChannelModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+ VideoChannel: VideoChannelModel
+
+ static load (id: number) {
+ return ActorModel.unscoped().findById(id)
+ }
+
static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query = {
where: {
transaction
}
- return ActorModel.findAll(query)
+ return ActorModel.scope(ScopeNames.FULL).findAll(query)
}
- toFormattedJSON () {
- let avatar: Avatar = null
- if (this.Avatar) {
- avatar = {
- path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
- createdAt: this.Avatar.createdAt,
- updatedAt: this.Avatar.updatedAt
+ static loadLocalByName (preferredUsername: string) {
+ const query = {
+ where: {
+ preferredUsername,
+ serverId: null
}
}
- let host = CONFIG.WEBSERVER.HOST
- let score: number
- if (this.Server) {
- host = this.Server.host
- score = this.Server.score
+ return ActorModel.scope(ScopeNames.FULL).findOne(query)
+ }
+
+ static loadByNameAndHost (preferredUsername: string, host: string) {
+ const query = {
+ where: {
+ preferredUsername
+ },
+ include: [
+ {
+ model: ServerModel,
+ required: true,
+ where: {
+ host
+ }
+ }
+ ]
+ }
+
+ return ActorModel.scope(ScopeNames.FULL).findOne(query)
+ }
+
+ static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
+ const query = {
+ where: {
+ url
+ },
+ transaction
+ }
+
+ return ActorModel.scope(ScopeNames.FULL).findOne(query)
+ }
+
+ toFormattedJSON () {
+ let avatar: Avatar = null
+ if (this.Avatar) {
+ avatar = this.Avatar.toFormattedJSON()
}
return {
id: this.id,
- host,
- score,
+ url: this.url,
+ uuid: this.uuid,
+ name: this.preferredUsername,
+ host: this.getHost(),
followingCount: this.followingCount,
followersCount: this.followersCount,
- avatar
+ avatar,
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt
}
}
- toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') {
+ toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
let activityPubType
if (type === 'Account') {
- activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
+ activityPubType = 'Person' as 'Person'
+ } else if (type === 'Application') {
+ activityPubType = 'Application' as 'Application'
} else { // VideoChannel
- activityPubType = 'Group'
+ activityPubType = 'Group' as 'Group'
+ }
+
+ let icon = undefined
+ if (this.avatarId) {
+ const extension = extname(this.Avatar.filename)
+ icon = {
+ type: 'Image',
+ mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
+ url: this.getAvatarUrl()
+ }
}
const json = {
- type,
+ type: activityPubType,
id: this.url,
following: this.getFollowingUrl(),
followers: this.getFollowersUrl(),
inbox: this.inboxUrl,
outbox: this.outboxUrl,
- preferredUsername: name,
+ preferredUsername: this.preferredUsername,
url: this.url,
name,
endpoints: {
sharedInbox: this.sharedInboxUrl
},
- uuid,
+ uuid: this.uuid,
publicKey: {
id: this.getPublicKeyUrl(),
owner: this.url,
publicKeyPem: this.publicKey
- }
+ },
+ icon
}
return activityPubContextify(json)
attributes: [ 'sharedInboxUrl' ],
include: [
{
- model: AccountFollowModel,
+ model: ActorFollowModel,
required: true,
as: 'followers',
where: {
- targetAccountId: this.id
+ targetActorId: this.id
}
}
],
isOwned () {
return this.serverId === null
}
+
+ getWebfingerUrl () {
+ return 'acct:' + this.preferredUsername + '@' + this.getHost()
+ }
+
+ getHost () {
+ return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
+ }
+
+ getAvatarUrl () {
+ if (!this.avatarId) return undefined
+
+ return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
+ }
+
+ isOutdated () {
+ if (this.isOwned()) return false
+
+ const now = Date.now()
+ const createdAtTime = this.createdAt.getTime()
+ const updatedAtTime = this.updatedAt.getTime()
+
+ return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
+ (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
+ }
}