123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- import { values } from 'lodash'
- import { extname } from 'path'
- import * as Sequelize from 'sequelize'
- import {
- 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/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, 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'
- import { VideoModel } from '../video/video'
- enum ScopeNames {
- FULL = 'FULL'
- }
- export const unusedActorAttributesForAPI = [
- 'publicKey',
- 'privateKey',
- 'inboxUrl',
- 'outboxUrl',
- 'sharedInboxUrl',
- 'followersUrl',
- 'followingUrl',
- 'url',
- 'createdAt',
- 'updatedAt'
- ]
- @DefaultScope({
- include: [
- {
- model: () => ServerModel,
- required: false
- },
- {
- model: () => AvatarModel,
- 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: () => AvatarModel,
- required: false
- }
- ]
- }
- })
- @Table({
- tableName: 'actor',
- indexes: [
- {
- fields: [ 'url' ],
- unique: true
- },
- {
- fields: [ 'preferredUsername', 'serverId' ],
- unique: true
- },
- {
- fields: [ 'inboxUrl', 'sharedInboxUrl' ]
- },
- {
- fields: [ 'sharedInboxUrl' ]
- },
- {
- fields: [ 'serverId' ]
- },
- {
- fields: [ 'avatarId' ]
- },
- {
- fields: [ 'uuid' ],
- unique: true
- },
- {
- fields: [ 'followersUrl' ]
- }
- ]
- })
- 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)
- @Column(DataType.UUID)
- uuid: string
- @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'))
- @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.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(false)
- @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
- @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.ACTORS.URL.max))
- sharedInboxUrl: string
- @AllowNull(false)
- @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
- @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.ACTORS.URL.max))
- followingUrl: string
- @CreatedAt
- createdAt: Date
- @UpdatedAt
- updatedAt: Date
- @ForeignKey(() => AvatarModel)
- @Column
- avatarId: number
- @BelongsTo(() => AvatarModel, {
- foreignKey: {
- allowNull: true
- },
- onDelete: 'set null',
- hooks: true
- })
- Avatar: AvatarModel
- @HasMany(() => ActorFollowModel, {
- foreignKey: {
- name: 'actorId',
- allowNull: false
- },
- 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) {
- return ActorModel.unscoped().findById(id)
- }
- static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) {
- 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 as any) // FIXME: typings
- }
- static isActorUrlExist (url: string) {
- const query = {
- raw: true,
- where: {
- url
- }
- }
- return ActorModel.unscoped().findOne(query)
- .then(a => !!a)
- }
- static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
- const query = {
- where: {
- followersUrl: {
- [ Sequelize.Op.in ]: followersUrls
- }
- },
- transaction
- }
- return ActorModel.scope(ScopeNames.FULL).findAll(query)
- }
- static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) {
- const query = {
- where: {
- preferredUsername,
- serverId: null
- },
- transaction
- }
- 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,
- 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?: Sequelize.Transaction) {
- const query = {
- where: {
- url
- },
- transaction
- }
- return ActorModel.scope(ScopeNames.FULL).findOne(query)
- }
- static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
- // FIXME: typings
- return (ActorModel as any).increment(column, {
- by,
- where: {
- id
- }
- })
- }
- toFormattedJSON () {
- let avatar: Avatar = null
- if (this.Avatar) {
- avatar = this.Avatar.toFormattedJSON()
- }
- return {
- id: this.id,
- url: this.url,
- uuid: this.uuid,
- name: this.preferredUsername,
- host: this.getHost(),
- hostRedundancyAllowed: this.getRedundancyAllowed(),
- followingCount: this.followingCount,
- followersCount: this.followersCount,
- avatar,
- createdAt: this.createdAt,
- updatedAt: this.updatedAt
- }
- }
- toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
- let activityPubType
- if (type === 'Account') {
- activityPubType = 'Person' as 'Person'
- } else if (type === 'Application') {
- activityPubType = 'Application' as 'Application'
- } else { // VideoChannel
- 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: activityPubType,
- id: this.url,
- following: this.getFollowingUrl(),
- followers: this.getFollowersUrl(),
- inbox: this.inboxUrl,
- outbox: this.outboxUrl,
- preferredUsername: this.preferredUsername,
- url: this.url,
- name,
- endpoints: {
- sharedInbox: this.sharedInboxUrl
- },
- uuid: this.uuid,
- publicKey: {
- id: this.getPublicKeyUrl(),
- owner: this.url,
- publicKeyPem: this.publicKey
- },
- icon
- }
- return activityPubContextify(json)
- }
- getFollowerSharedInboxUrls (t: Sequelize.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'
- }
- getPublicKeyUrl () {
- return this.url + '#main-key'
- }
- isOwned () {
- return this.serverId === null
- }
- getWebfingerUrl () {
- 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
- }
- getRedundancyAllowed () {
- return this.Server ? this.Server.redundancyAllowed : false
- }
- 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
- }
- }
|