From 50d6de9c286abcb34ff4234d56d9cbb803db7665 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 14 Dec 2017 17:38:41 +0100 Subject: Begin moving video channel to actor --- server/models/activitypub/actor-follow.ts | 260 ++++++++++++++++++++++++++++++ server/models/activitypub/actor.ts | 165 ++++++++++++++++--- 2 files changed, 402 insertions(+), 23 deletions(-) create mode 100644 server/models/activitypub/actor-follow.ts (limited to 'server/models/activitypub') diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts new file mode 100644 index 000000000..4cba05e95 --- /dev/null +++ b/server/models/activitypub/actor-follow.ts @@ -0,0 +1,260 @@ +import * as Bluebird from 'bluebird' +import { values } from 'lodash' +import * as Sequelize from 'sequelize' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { FollowState } from '../../../shared/models/actors' +import { FOLLOW_STATES } from '../../initializers/constants' +import { ServerModel } from '../server/server' +import { getSort } from '../utils' +import { ActorModel } from './actor' + +@Table({ + tableName: 'actorFollow', + indexes: [ + { + fields: [ 'actorId' ] + }, + { + fields: [ 'targetActorId' ] + }, + { + fields: [ 'actorId', 'targetActorId' ], + unique: true + } + ] +}) +export class ActorFollowModel extends Model { + + @AllowNull(false) + @Column(DataType.ENUM(values(FOLLOW_STATES))) + state: FollowState + + @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 + + static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { + 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 loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) { + const query = { + where: { + actorId + }, + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower' + }, + { + model: ActorModel, + required: true, + as: 'ActorFollowing', + include: [ + { + model: ServerModel, + required: true, + where: { + host: targetHost + } + } + ] + } + ], + transaction: t + } + + return ActorFollowModel.findOne(query) + } + + static listFollowingForApi (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower', + where: { + id + } + }, + { + model: ActorModel, + as: 'ActorFollowing', + required: true, + include: [ ServerModel ] + } + ] + } + + return ActorFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listFollowersForApi (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: ActorModel, + required: true, + as: 'ActorFollower', + include: [ ServerModel ] + }, + { + model: ActorModel, + as: 'ActorFollowing', + required: true, + where: { + id + } + } + ] + } + + return ActorFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + + static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { + return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) + } + + static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) { + return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, undefined, undefined, 'sharedInboxUrl') + } + + static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { + return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) + } + + private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following', + actorIds: number[], + t: Sequelize.Transaction, + start?: number, + count?: number, + columnUrl = 'url') { + let firstJoin: string + let secondJoin: string + + if (type === 'followers') { + firstJoin = 'targetActorId' + secondJoin = 'actorId' + } else { + firstJoin = 'actorId' + secondJoin = 'targetActorId' + } + + const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] + const tasks: Bluebird[] = [] + + 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\' ' + + if (count !== undefined) query += 'LIMIT ' + count + if (start !== undefined) query += ' OFFSET ' + start + + const options = { + bind: { actorIds }, + type: Sequelize.QueryTypes.SELECT, + transaction: t + } + tasks.push(ActorFollowModel.sequelize.query(query, options)) + } + + const [ followers, [ { total } ] ] = await + Promise.all(tasks) + const urls: string[] = followers.map(f => f.url) + + return { + data: urls, + total: parseInt(total, 10) + } + } + + toFormattedJSON () { + const follower = this.ActorFollower.toFormattedJSON() + const following = this.ActorFollowing.toFormattedJSON() + + return { + id: this.id, + follower, + following, + state: this.state, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } +} diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 4cae6a6ec..ecaa43dcf 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -1,30 +1,75 @@ +import { values } from 'lodash' import { join } from 'path' import * as Sequelize from 'sequelize' import { - AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table, + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + 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 { isActivityPubUrlValid, isActorFollowersCountValid, - isActorFollowingCountValid, isActorPreferredUsernameValid, + isActorFollowingCountValid, + isActorNameValid, 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' +import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, 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' +} + +@Scopes({ + [ScopeNames.FULL]: { + include: [ + { + model: () => AccountModel, + required: false + }, + { + model: () => VideoChannelModel, + required: false + } + ] + } +}) @Table({ - tableName: 'actor' + tableName: 'actor', + indexes: [ + { + fields: [ 'name', 'serverId' ], + unique: true + } + ] }) export class ActorModel extends Model { + @AllowNull(false) + @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES))) + type: ActivityPubActorType + @AllowNull(false) @Default(DataType.UUIDV4) @IsUUID(4) @@ -32,7 +77,7 @@ export class ActorModel extends Model { uuid: string @AllowNull(false) - @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name')) + @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name')) @Column name: string @@ -104,24 +149,24 @@ export class ActorModel extends Model { }) 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 @@ -135,6 +180,36 @@ export class ActorModel extends Model { }) 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.scope(ScopeNames.FULL).findById(id) + } + + static loadByUUID (uuid: string) { + const query = { + where: { + uuid + } + } + + return ActorModel.scope(ScopeNames.FULL).findOne(query) + } + static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { const query = { where: { @@ -145,7 +220,48 @@ export class ActorModel extends Model { transaction } - return ActorModel.findAll(query) + return ActorModel.scope(ScopeNames.FULL).findAll(query) + } + + static loadLocalByName (name: string) { + const query = { + where: { + name, + serverId: null + } + } + + return ActorModel.scope(ScopeNames.FULL).findOne(query) + } + + static loadByNameAndHost (name: string, host: string) { + const query = { + where: { + name + }, + 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 () { @@ -167,6 +283,7 @@ export class ActorModel extends Model { return { id: this.id, + uuid: this.uuid, host, score, followingCount: this.followingCount, @@ -175,28 +292,30 @@ export class ActorModel extends Model { } } - toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') { + toActivityPubObject (preferredUsername: 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' } const json = { - type, + type: activityPubType, id: this.url, following: this.getFollowingUrl(), followers: this.getFollowersUrl(), inbox: this.inboxUrl, outbox: this.outboxUrl, - preferredUsername: name, + preferredUsername, url: this.url, - name, + name: this.name, endpoints: { sharedInbox: this.sharedInboxUrl }, - uuid, + uuid: this.uuid, publicKey: { id: this.getPublicKeyUrl(), owner: this.url, @@ -212,11 +331,11 @@ export class ActorModel extends Model { attributes: [ 'sharedInboxUrl' ], include: [ { - model: AccountFollowModel, + model: ActorFollowModel, required: true, as: 'followers', where: { - targetAccountId: this.id + targetActorId: this.id } } ], -- cgit v1.2.3