X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Factivitypub%2Factor.ts;h=4a466441cd98a73c034ace260279a884d38aeb34;hb=3acc50844047a37698f0618fa235c138e386a053;hp=ecaa43dcf1203d05a17f12714ea398253e8136f6;hpb=50d6de9c286abcb34ff4234d56d9cbb803db7665;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index ecaa43dcf..4a466441c 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -1,5 +1,5 @@ import { values } from 'lodash' -import { join } from 'path' +import { extname } from 'path' import * as Sequelize from 'sequelize' import { AllowNull, @@ -8,6 +8,7 @@ import { CreatedAt, DataType, Default, + DefaultScope, ForeignKey, HasMany, HasOne, @@ -20,54 +21,117 @@ import { } 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, - isActorNameValid, + isActorPreferredUsernameValid, isActorPrivateKeyValid, isActorPublicKeyValid -} from '../../helpers/custom-validators/activitypub' -import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' +} from '../../helpers/custom-validators/activitypub/actor' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' +import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' import { AccountModel } from '../account/account' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { throwIfNotValid } from '../utils' +import { isOutdated, throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' import { ActorFollowModel } from './actor-follow' +import { VideoModel } from '../video/video' enum ScopeNames { FULL = 'FULL' } -@Scopes({ +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, + model: AccountModel.unscoped(), + required: false + }, + { + model: VideoChannelModel.unscoped(), + required: false, + include: [ + { + model: AccountModel, + required: true + } + ] + }, + { + model: ServerModel, required: false }, { - model: () => VideoChannelModel, + model: AvatarModel, required: false } ] } -}) +})) @Table({ tableName: 'actor', indexes: [ { - fields: [ 'name', 'serverId' ], + 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 { @AllowNull(false) - @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES))) + @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) type: ActivityPubActorType @AllowNull(false) @@ -77,23 +141,23 @@ export class ActorModel extends Model { uuid: string @AllowNull(false) - @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, '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)) + @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')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY.max)) + @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max)) privateKey: string @AllowNull(false) @@ -108,27 +172,27 @@ export class ActorModel extends Model { @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 @@ -145,7 +209,8 @@ export class ActorModel extends Model { foreignKey: { allowNull: true }, - onDelete: 'cascade' + onDelete: 'set null', + hooks: true }) Avatar: AvatarModel @@ -154,19 +219,20 @@ export class ActorModel extends Model { name: 'actorId', allowNull: false }, + as: 'ActorFollowings', onDelete: 'cascade' }) - AccountFollowing: ActorFollowModel[] + ActorFollowing: ActorFollowModel[] @HasMany(() => ActorFollowModel, { foreignKey: { name: 'targetActorId', allowNull: false }, - as: 'followers', + as: 'ActorFollowers', onDelete: 'cascade' }) - AccountFollowers: ActorFollowModel[] + ActorFollowers: ActorFollowModel[] @ForeignKey(() => ServerModel) @Column @@ -184,7 +250,8 @@ export class ActorModel extends Model { foreignKey: { allowNull: true }, - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) Account: AccountModel @@ -192,22 +259,57 @@ export class ActorModel extends Model { foreignKey: { allowNull: true }, - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) VideoChannel: VideoChannelModel static load (id: number) { - return ActorModel.scope(ScopeNames.FULL).findById(id) + return ActorModel.unscoped().findByPk(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) } - static loadByUUID (uuid: string) { + static isActorUrlExist (url: string) { const query = { + raw: true, where: { - uuid + url } } - return ActorModel.scope(ScopeNames.FULL).findOne(query) + return ActorModel.unscoped().findOne(query) + .then(a => !!a) } static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { @@ -223,21 +325,22 @@ export class ActorModel extends Model { return ActorModel.scope(ScopeNames.FULL).findAll(query) } - static loadLocalByName (name: string) { + static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) { const query = { where: { - name, + preferredUsername, serverId: null - } + }, + transaction } return ActorModel.scope(ScopeNames.FULL).findOne(query) } - static loadByNameAndHost (name: string, host: string) { + static loadByNameAndHost (preferredUsername: string, host: string) { const query = { where: { - name + preferredUsername }, include: [ { @@ -254,6 +357,29 @@ export class ActorModel extends Model { } 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 @@ -264,35 +390,37 @@ export class ActorModel extends Model { return ActorModel.scope(ScopeNames.FULL).findOne(query) } + static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) { + return ActorModel.increment(column, { + by, + where: { + id + } + }) + } + 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 - } - } - - let host = CONFIG.WEBSERVER.HOST - let score: number - if (this.Server) { - host = this.Server.host - score = this.Server.score + avatar = this.Avatar.toFormattedJSON() } return { id: this.id, + url: this.url, uuid: this.uuid, - host, - score, + name: this.preferredUsername, + host: this.getHost(), + hostRedundancyAllowed: this.getRedundancyAllowed(), followingCount: this.followingCount, followersCount: this.followersCount, - avatar + avatar, + createdAt: this.createdAt, + updatedAt: this.updatedAt } } - toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') { + toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') { let activityPubType if (type === 'Account') { activityPubType = 'Person' as 'Person' @@ -302,16 +430,27 @@ export class ActorModel extends Model { 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(), + playlists: this.getPlaylistsUrl(), inbox: this.inboxUrl, outbox: this.outboxUrl, - preferredUsername, + preferredUsername: this.preferredUsername, url: this.url, - name: this.name, + name, endpoints: { sharedInbox: this.sharedInboxUrl }, @@ -320,7 +459,8 @@ export class ActorModel extends Model { id: this.getPublicKeyUrl(), owner: this.url, publicKeyPem: this.publicKey - } + }, + icon } return activityPubContextify(json) @@ -331,10 +471,12 @@ export class ActorModel extends Model { attributes: [ 'sharedInboxUrl' ], include: [ { - model: ActorFollowModel, + attribute: [], + model: ActorFollowModel.unscoped(), required: true, - as: 'followers', + as: 'ActorFollowing', where: { + state: 'accepted', targetActorId: this.id } } @@ -354,6 +496,10 @@ export class ActorModel extends Model { return this.url + '/followers' } + getPlaylistsUrl () { + return this.url + '/playlists' + } + getPublicKeyUrl () { return this.url + '#main-key' } @@ -361,4 +507,32 @@ export class ActorModel extends Model { 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 : WEBSERVER.HOST + } + + getRedundancyAllowed () { + return this.Server ? this.Server.redundancyAllowed : false + } + + getAvatarUrl () { + if (!this.avatarId) return undefined + + return WEBSERVER.URL + this.Avatar.getWebserverPath() + } + + isOutdated () { + if (this.isOwned()) return false + + return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL) + } }