diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index eda3403f2..2c64efe1f 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -9,7 +9,7 @@ import { isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 170 +const LAST_MIGRATION_VERSION = 175 // --------------------------------------------------------------------------- diff --git a/server/initializers/migrations/0175-actor-follow-counts.ts b/server/initializers/migrations/0175-actor-follow-counts.ts new file mode 100644 index 000000000..06ef77b49 --- /dev/null +++ b/server/initializers/migrations/0175-actor-follow-counts.ts @@ -0,0 +1,24 @@ +import * as Sequelize from 'sequelize' +import { ACTOR_FOLLOW_SCORE } from '../index' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { + const query = 'UPDATE "actor" SET ' + + '"followersCount" = (SELECT COUNT(*) FROM "actorFollow" WHERE "actor"."id" = "actorFollow"."targetActorId"), ' + + '"followingCount" = (SELECT COUNT(*) FROM "actorFollow" WHERE "actor"."id" = "actorFollow"."actorId") ' + + 'WHERE "actor"."serverId" IS NULL' + + await utils.sequelize.query(query) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 1e1eab54a..b3fb75421 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -314,6 +314,7 @@ async function refreshActorIfNeeded (actor: ActorModel) { if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.') return sequelizeTypescript.transaction(async t => { + logger.info('coucou', result.actor.toJSON()) updateInstanceWithAnother(actor, result.actor) if (result.avatarName !== undefined) { diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 5085c5da9..69f5c51b5 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -51,6 +51,9 @@ async function follow (actor: ActorModel, targetActorURL: string) { transaction: t }) + actorFollow.ActorFollower = actor + actorFollow.ActorFollowing = targetActor + if (actorFollow.state !== 'accepted') { actorFollow.state = 'accepted' await actorFollow.save({ transaction: t }) diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index de5bb6f74..435d22db5 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird' import { values } from 'lodash' import * as Sequelize from 'sequelize' import { + AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, Table, UpdatedAt } from 'sequelize-typescript' @@ -79,6 +80,25 @@ export class ActorFollowModel extends Model { }) ActorFollowing: ActorModel + @AfterCreate + @AfterUpdate + static incrementFollowerAndFollowingCount (instance: ActorFollowModel) { + if (instance.state !== 'accepted') return + + return Promise.all([ + ActorModel.incrementFollows(instance.actorId, 'followingCount', 1), + ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1) + ]) + } + + @AfterDestroy + static decrementFollowerAndFollowingCount (instance: ActorFollowModel) { + return Promise.all([ + ActorModel.incrementFollows(instance.actorId, 'followingCount',-1), + ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1) + ]) + } + // Remove actor follows with a score of 0 (too many requests where they were unreachable) static async removeBadActorFollows () { const actorFollows = await ActorFollowModel.listBadActorFollows() diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 17f69f7a7..b7be9c32c 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -264,6 +264,16 @@ export class ActorModel extends Model { 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) { diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index e6dfd5f62..27cf94985 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts @@ -12,6 +12,7 @@ import { } from '../../utils/index' import { dateIsValid } from '../../utils/miscs/miscs' import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort, unfollow } from '../../utils/server/follows' +import { expectAccountFollows } from '../../utils/users/accounts' import { userLogin } from '../../utils/users/login' import { createUser } from '../../utils/users/users' import { @@ -116,6 +117,19 @@ describe('Test follows', function () { expect(follows.length).to.equal(0) }) + it('Should have the correct following counts', async function () { + await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 2) + await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) + await expectAccountFollows(servers[0].url, 'peertube@localhost:9003', 1, 0) + + // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) + await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) + + await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 1, 0) + }) + it('Should unfollow server 3 on server 1', async function () { this.timeout(5000) @@ -144,6 +158,17 @@ describe('Test follows', function () { expect(follows.length).to.equal(0) }) + it('Should have the correct following counts 2', async function () { + await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) + + await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) + + await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 0) + await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 0, 0) + }) + it('Should upload a video on server 2 ans 3 and propagate only the video of server 2', async function () { this.timeout(10000) @@ -223,6 +248,18 @@ describe('Test follows', function () { await wait(7000) }) + it('Should have the correct following counts 2', async function () { + await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 2) + await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) + await expectAccountFollows(servers[0].url, 'peertube@localhost:9003', 1, 0) + + await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) + + await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 1) + await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 1, 0) + }) + it('Should propagate videos', async function () { const res = await getVideosList(servers[ 0 ].url) expect(res.body.total).to.equal(7) diff --git a/server/tests/utils/users/accounts.ts b/server/tests/utils/users/accounts.ts index 71712100e..0ec7992b3 100644 --- a/server/tests/utils/users/accounts.ts +++ b/server/tests/utils/users/accounts.ts @@ -1,3 +1,5 @@ +import { expect } from 'chai' +import { Account } from '../../../../shared/models/actors' import { makeGetRequest } from '../requests/requests' function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) { @@ -21,9 +23,19 @@ function getAccount (url: string, accountId: number | string, statusCodeExpected }) } +async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) { + const res = await getAccountsList(url) + const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain) + + const message = `${nameWithDomain} on ${url}` + expect(account.followersCount).to.equal(followersCount, message) + expect(account.followingCount).to.equal(followingCount, message) +} + // --------------------------------------------------------------------------- export { getAccount, + expectAccountFollows, getAccountsList }