aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/server/follows.ts4
-rw-r--r--server/lib/activitypub/send/send-undo.ts2
-rw-r--r--server/models/actor/actor-follow.ts146
-rw-r--r--server/models/actor/sql/instance-list-followers-query-builder.ts69
-rw-r--r--server/models/actor/sql/instance-list-following-query-builder.ts69
-rw-r--r--server/models/actor/sql/shared/actor-follow-table-attributes.ts62
-rw-r--r--server/models/actor/sql/shared/instance-list-follows-query-builder.ts97
-rw-r--r--server/models/shared/abstract-run-query.ts4
-rw-r--r--server/models/utils.ts15
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts4
-rw-r--r--server/tests/api/redundancy/manage-redundancy.ts11
11 files changed, 338 insertions, 145 deletions
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index c613386b2..9557810b5 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -99,7 +99,7 @@ export {
99async function listFollowing (req: express.Request, res: express.Response) { 99async function listFollowing (req: express.Request, res: express.Response) {
100 const serverActor = await getServerActor() 100 const serverActor = await getServerActor()
101 const resultList = await ActorFollowModel.listInstanceFollowingForApi({ 101 const resultList = await ActorFollowModel.listInstanceFollowingForApi({
102 id: serverActor.id, 102 followerId: serverActor.id,
103 start: req.query.start, 103 start: req.query.start,
104 count: req.query.count, 104 count: req.query.count,
105 sort: req.query.sort, 105 sort: req.query.sort,
@@ -159,7 +159,7 @@ async function removeFollowing (req: express.Request, res: express.Response) {
159 const follow = res.locals.follow 159 const follow = res.locals.follow
160 160
161 await sequelizeTypescript.transaction(async t => { 161 await sequelizeTypescript.transaction(async t => {
162 if (follow.state === 'accepted') await sendUndoFollow(follow, t) 162 if (follow.state === 'accepted') sendUndoFollow(follow, t)
163 163
164 // Disable redundancy on unfollowed instances 164 // Disable redundancy on unfollowed instances
165 const server = follow.ActorFollowing.Server 165 const server = follow.ActorFollowing.Server
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index 36d7ef991..442178c42 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -44,7 +44,7 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
44 const followActivity = buildFollowActivity(actorFollow.url, me, following) 44 const followActivity = buildFollowActivity(actorFollow.url, me, following)
45 const undoActivity = undoActivityData(undoUrl, me, followActivity) 45 const undoActivity = undoActivityData(undoUrl, me, followActivity)
46 46
47 return t.afterCommit(() => { 47 t.afterCommit(() => {
48 return unicastTo({ 48 return unicastTo({
49 data: undoActivity, 49 data: undoActivity,
50 byActor: me, 50 byActor: me,
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts
index 0f4d3c0a6..af1d85e9f 100644
--- a/server/models/actor/actor-follow.ts
+++ b/server/models/actor/actor-follow.ts
@@ -1,5 +1,5 @@
1import { difference, values } from 'lodash' 1import { difference, values } from 'lodash'
2import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' 2import { Includeable, IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize'
3import { 3import {
4 AfterCreate, 4 AfterCreate,
5 AfterDestroy, 5 AfterDestroy,
@@ -30,7 +30,6 @@ import {
30 MActorFollowFormattable, 30 MActorFollowFormattable,
31 MActorFollowSubscriptions 31 MActorFollowSubscriptions
32} from '@server/types/models' 32} from '@server/types/models'
33import { ActivityPubActorType } from '@shared/models'
34import { AttributesOnly } from '@shared/typescript-utils' 33import { AttributesOnly } from '@shared/typescript-utils'
35import { FollowState } from '../../../shared/models/actors' 34import { FollowState } from '../../../shared/models/actors'
36import { ActorFollow } from '../../../shared/models/actors/follow.model' 35import { ActorFollow } from '../../../shared/models/actors/follow.model'
@@ -39,9 +38,11 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM
39import { AccountModel } from '../account/account' 38import { AccountModel } from '../account/account'
40import { ServerModel } from '../server/server' 39import { ServerModel } from '../server/server'
41import { doesExist } from '../shared/query' 40import { doesExist } from '../shared/query'
42import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' 41import { createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils'
43import { VideoChannelModel } from '../video/video-channel' 42import { VideoChannelModel } from '../video/video-channel'
44import { ActorModel, unusedActorAttributesForAPI } from './actor' 43import { ActorModel, unusedActorAttributesForAPI } from './actor'
44import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder'
45import { InstanceListFollowingQueryBuilder, ListFollowingOptions } from './sql/instance-list-following-query-builder'
45 46
46@Table({ 47@Table({
47 tableName: 'actorFollow', 48 tableName: 'actorFollow',
@@ -348,144 +349,17 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
348 return ActorFollowModel.findAll(query) 349 return ActorFollowModel.findAll(query)
349 } 350 }
350 351
351 static listInstanceFollowingForApi (options: { 352 static listInstanceFollowingForApi (options: ListFollowingOptions) {
352 id: number
353 start: number
354 count: number
355 sort: string
356 state?: FollowState
357 actorType?: ActivityPubActorType
358 search?: string
359 }) {
360 const { id, start, count, sort, search, state, actorType } = options
361
362 const followWhere = state ? { state } : {}
363 const followingWhere: WhereOptions = {}
364
365 if (search) {
366 Object.assign(followWhere, {
367 [Op.or]: [
368 searchAttribute(options.search, '$ActorFollowing.preferredUsername$'),
369 searchAttribute(options.search, '$ActorFollowing.Server.host$')
370 ]
371 })
372 }
373
374 if (actorType) {
375 Object.assign(followingWhere, { type: actorType })
376 }
377
378 const getQuery = (forCount: boolean) => {
379 const actorModel = forCount
380 ? ActorModel.unscoped()
381 : ActorModel
382
383 return {
384 distinct: true,
385 offset: start,
386 limit: count,
387 order: getFollowsSort(sort),
388 where: followWhere,
389 include: [
390 {
391 model: actorModel,
392 required: true,
393 as: 'ActorFollower',
394 where: {
395 id
396 }
397 },
398 {
399 model: actorModel,
400 as: 'ActorFollowing',
401 required: true,
402 where: followingWhere,
403 include: [
404 {
405 model: ServerModel,
406 required: true
407 }
408 ]
409 }
410 ]
411 }
412 }
413
414 return Promise.all([ 353 return Promise.all([
415 ActorFollowModel.count(getQuery(true)), 354 new InstanceListFollowingQueryBuilder(this.sequelize, options).countFollowing(),
416 ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) 355 new InstanceListFollowingQueryBuilder(this.sequelize, options).listFollowing()
417 ]).then(([ total, data ]) => ({ total, data })) 356 ]).then(([ total, data ]) => ({ total, data }))
418 } 357 }
419 358
420 static listFollowersForApi (options: { 359 static listFollowersForApi (options: ListFollowersOptions) {
421 actorIds: number[]
422 start: number
423 count: number
424 sort: string
425 state?: FollowState
426 actorType?: ActivityPubActorType
427 search?: string
428 }) {
429 const { actorIds, start, count, sort, search, state, actorType } = options
430
431 const followWhere = state ? { state } : {}
432 const followerWhere: WhereOptions = {}
433
434 if (search) {
435 const escapedSearch = ActorFollowModel.sequelize.escape('%' + search + '%')
436
437 Object.assign(followerWhere, {
438 id: {
439 [Op.in]: literal(
440 `(` +
441 `SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" ` +
442 `WHERE "preferredUsername" ILIKE ${escapedSearch} OR "host" ILIKE ${escapedSearch}` +
443 `)`
444 )
445 }
446 })
447 }
448
449 if (actorType) {
450 Object.assign(followerWhere, { type: actorType })
451 }
452
453 const getQuery = (forCount: boolean) => {
454 const actorModel = forCount
455 ? ActorModel.unscoped()
456 : ActorModel
457
458 return {
459 distinct: true,
460
461 offset: start,
462 limit: count,
463 order: getFollowsSort(sort),
464 where: followWhere,
465 include: [
466 {
467 model: actorModel,
468 required: true,
469 as: 'ActorFollower',
470 where: followerWhere
471 },
472 {
473 model: actorModel,
474 as: 'ActorFollowing',
475 required: true,
476 where: {
477 id: {
478 [Op.in]: actorIds
479 }
480 }
481 }
482 ]
483 }
484 }
485
486 return Promise.all([ 360 return Promise.all([
487 ActorFollowModel.count(getQuery(true)), 361 new InstanceListFollowersQueryBuilder(this.sequelize, options).countFollowers(),
488 ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) 362 new InstanceListFollowersQueryBuilder(this.sequelize, options).listFollowers()
489 ]).then(([ total, data ]) => ({ total, data })) 363 ]).then(([ total, data ]) => ({ total, data }))
490 } 364 }
491 365
diff --git a/server/models/actor/sql/instance-list-followers-query-builder.ts b/server/models/actor/sql/instance-list-followers-query-builder.ts
new file mode 100644
index 000000000..4a17a8f11
--- /dev/null
+++ b/server/models/actor/sql/instance-list-followers-query-builder.ts
@@ -0,0 +1,69 @@
1import { Sequelize } from 'sequelize'
2import { ModelBuilder } from '@server/models/shared'
3import { parseRowCountResult } from '@server/models/utils'
4import { MActorFollowActorsDefault } from '@server/types/models'
5import { ActivityPubActorType, FollowState } from '@shared/models'
6import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder'
7
8export interface ListFollowersOptions {
9 actorIds: number[]
10 start: number
11 count: number
12 sort: string
13 state?: FollowState
14 actorType?: ActivityPubActorType
15 search?: string
16}
17
18export class InstanceListFollowersQueryBuilder extends InstanceListFollowsQueryBuilder <ListFollowersOptions> {
19
20 constructor (
21 protected readonly sequelize: Sequelize,
22 protected readonly options: ListFollowersOptions
23 ) {
24 super(sequelize, options)
25 }
26
27 async listFollowers () {
28 this.buildListQuery()
29
30 const results = await this.runQuery({ nest: true })
31 const modelBuilder = new ModelBuilder<MActorFollowActorsDefault>(this.sequelize)
32
33 return modelBuilder.createModels(results, 'ActorFollow')
34 }
35
36 async countFollowers () {
37 this.buildCountQuery()
38
39 const result = await this.runQuery()
40
41 return parseRowCountResult(result)
42 }
43
44 protected getWhere () {
45 let where = 'WHERE "ActorFollowing"."id" IN (:actorIds) '
46 this.replacements.actorIds = this.options.actorIds
47
48 if (this.options.state) {
49 where += 'AND "ActorFollowModel"."state" = :state '
50 this.replacements.state = this.options.state
51 }
52
53 if (this.options.search) {
54 const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%')
55
56 where += `AND (` +
57 `"ActorFollower->Server"."host" ILIKE ${escapedLikeSearch} ` +
58 `OR "ActorFollower"."preferredUsername" ILIKE ${escapedLikeSearch} ` +
59 `)`
60 }
61
62 if (this.options.actorType) {
63 where += `AND "ActorFollower"."type" = :actorType `
64 this.replacements.actorType = this.options.actorType
65 }
66
67 return where
68 }
69}
diff --git a/server/models/actor/sql/instance-list-following-query-builder.ts b/server/models/actor/sql/instance-list-following-query-builder.ts
new file mode 100644
index 000000000..880170b85
--- /dev/null
+++ b/server/models/actor/sql/instance-list-following-query-builder.ts
@@ -0,0 +1,69 @@
1import { Sequelize } from 'sequelize'
2import { ModelBuilder } from '@server/models/shared'
3import { parseRowCountResult } from '@server/models/utils'
4import { MActorFollowActorsDefault } from '@server/types/models'
5import { ActivityPubActorType, FollowState } from '@shared/models'
6import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder'
7
8export interface ListFollowingOptions {
9 followerId: number
10 start: number
11 count: number
12 sort: string
13 state?: FollowState
14 actorType?: ActivityPubActorType
15 search?: string
16}
17
18export class InstanceListFollowingQueryBuilder extends InstanceListFollowsQueryBuilder <ListFollowingOptions> {
19
20 constructor (
21 protected readonly sequelize: Sequelize,
22 protected readonly options: ListFollowingOptions
23 ) {
24 super(sequelize, options)
25 }
26
27 async listFollowing () {
28 this.buildListQuery()
29
30 const results = await this.runQuery({ nest: true })
31 const modelBuilder = new ModelBuilder<MActorFollowActorsDefault>(this.sequelize)
32
33 return modelBuilder.createModels(results, 'ActorFollow')
34 }
35
36 async countFollowing () {
37 this.buildCountQuery()
38
39 const result = await this.runQuery()
40
41 return parseRowCountResult(result)
42 }
43
44 protected getWhere () {
45 let where = 'WHERE "ActorFollowModel"."actorId" = :followerId '
46 this.replacements.followerId = this.options.followerId
47
48 if (this.options.state) {
49 where += 'AND "ActorFollowModel"."state" = :state '
50 this.replacements.state = this.options.state
51 }
52
53 if (this.options.search) {
54 const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%')
55
56 where += `AND (` +
57 `"ActorFollowing->Server"."host" ILIKE ${escapedLikeSearch} ` +
58 `OR "ActorFollowing"."preferredUsername" ILIKE ${escapedLikeSearch} ` +
59 `)`
60 }
61
62 if (this.options.actorType) {
63 where += `AND "ActorFollowing"."type" = :actorType `
64 this.replacements.actorType = this.options.actorType
65 }
66
67 return where
68 }
69}
diff --git a/server/models/actor/sql/shared/actor-follow-table-attributes.ts b/server/models/actor/sql/shared/actor-follow-table-attributes.ts
new file mode 100644
index 000000000..156b37d44
--- /dev/null
+++ b/server/models/actor/sql/shared/actor-follow-table-attributes.ts
@@ -0,0 +1,62 @@
1export class ActorFollowTableAttributes {
2
3 getFollowAttributes () {
4 return [
5 '"ActorFollowModel"."id"',
6 '"ActorFollowModel"."state"',
7 '"ActorFollowModel"."score"',
8 '"ActorFollowModel"."url"',
9 '"ActorFollowModel"."actorId"',
10 '"ActorFollowModel"."targetActorId"',
11 '"ActorFollowModel"."createdAt"',
12 '"ActorFollowModel"."updatedAt"'
13 ].join(', ')
14 }
15
16 getActorAttributes (actorTableName: string) {
17 return [
18 `"${actorTableName}"."id" AS "${actorTableName}.id"`,
19 `"${actorTableName}"."type" AS "${actorTableName}.type"`,
20 `"${actorTableName}"."preferredUsername" AS "${actorTableName}.preferredUsername"`,
21 `"${actorTableName}"."url" AS "${actorTableName}.url"`,
22 `"${actorTableName}"."publicKey" AS "${actorTableName}.publicKey"`,
23 `"${actorTableName}"."privateKey" AS "${actorTableName}.privateKey"`,
24 `"${actorTableName}"."followersCount" AS "${actorTableName}.followersCount"`,
25 `"${actorTableName}"."followingCount" AS "${actorTableName}.followingCount"`,
26 `"${actorTableName}"."inboxUrl" AS "${actorTableName}.inboxUrl"`,
27 `"${actorTableName}"."outboxUrl" AS "${actorTableName}.outboxUrl"`,
28 `"${actorTableName}"."sharedInboxUrl" AS "${actorTableName}.sharedInboxUrl"`,
29 `"${actorTableName}"."followersUrl" AS "${actorTableName}.followersUrl"`,
30 `"${actorTableName}"."followingUrl" AS "${actorTableName}.followingUrl"`,
31 `"${actorTableName}"."remoteCreatedAt" AS "${actorTableName}.remoteCreatedAt"`,
32 `"${actorTableName}"."serverId" AS "${actorTableName}.serverId"`,
33 `"${actorTableName}"."createdAt" AS "${actorTableName}.createdAt"`,
34 `"${actorTableName}"."updatedAt" AS "${actorTableName}.updatedAt"`
35 ].join(', ')
36 }
37
38 getServerAttributes (actorTableName: string) {
39 return [
40 `"${actorTableName}->Server"."id" AS "${actorTableName}.Server.id"`,
41 `"${actorTableName}->Server"."host" AS "${actorTableName}.Server.host"`,
42 `"${actorTableName}->Server"."redundancyAllowed" AS "${actorTableName}.Server.redundancyAllowed"`,
43 `"${actorTableName}->Server"."createdAt" AS "${actorTableName}.Server.createdAt"`,
44 `"${actorTableName}->Server"."updatedAt" AS "${actorTableName}.Server.updatedAt"`
45 ].join(', ')
46 }
47
48 getAvatarAttributes (actorTableName: string) {
49 return [
50 `"${actorTableName}->Avatars"."id" AS "${actorTableName}.Avatars.id"`,
51 `"${actorTableName}->Avatars"."filename" AS "${actorTableName}.Avatars.filename"`,
52 `"${actorTableName}->Avatars"."height" AS "${actorTableName}.Avatars.height"`,
53 `"${actorTableName}->Avatars"."width" AS "${actorTableName}.Avatars.width"`,
54 `"${actorTableName}->Avatars"."fileUrl" AS "${actorTableName}.Avatars.fileUrl"`,
55 `"${actorTableName}->Avatars"."onDisk" AS "${actorTableName}.Avatars.onDisk"`,
56 `"${actorTableName}->Avatars"."type" AS "${actorTableName}.Avatars.type"`,
57 `"${actorTableName}->Avatars"."actorId" AS "${actorTableName}.Avatars.actorId"`,
58 `"${actorTableName}->Avatars"."createdAt" AS "${actorTableName}.Avatars.createdAt"`,
59 `"${actorTableName}->Avatars"."updatedAt" AS "${actorTableName}.Avatars.updatedAt"`
60 ].join(', ')
61 }
62}
diff --git a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts
new file mode 100644
index 000000000..1d70fbe70
--- /dev/null
+++ b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts
@@ -0,0 +1,97 @@
1import { Sequelize } from 'sequelize'
2import { AbstractRunQuery } from '@server/models/shared'
3import { getInstanceFollowsSort } from '@server/models/utils'
4import { ActorImageType } from '@shared/models'
5import { ActorFollowTableAttributes } from './actor-follow-table-attributes'
6
7type BaseOptions = {
8 sort: string
9 count: number
10 start: number
11}
12
13export abstract class InstanceListFollowsQueryBuilder <T extends BaseOptions> extends AbstractRunQuery {
14 protected readonly tableAttributes = new ActorFollowTableAttributes()
15
16 protected innerQuery: string
17
18 constructor (
19 protected readonly sequelize: Sequelize,
20 protected readonly options: T
21 ) {
22 super(sequelize)
23 }
24
25 protected abstract getWhere (): string
26
27 protected getJoins () {
28 return 'INNER JOIN "actor" "ActorFollower" ON "ActorFollower"."id" = "ActorFollowModel"."actorId" ' +
29 'INNER JOIN "actor" "ActorFollowing" ON "ActorFollowing"."id" = "ActorFollowModel"."targetActorId" '
30 }
31
32 protected getServerJoin (actorName: string) {
33 return `LEFT JOIN "server" "${actorName}->Server" ON "${actorName}"."serverId" = "${actorName}->Server"."id" `
34 }
35
36 protected getAvatarsJoin (actorName: string) {
37 return `LEFT JOIN "actorImage" "${actorName}->Avatars" ON "${actorName}.id" = "${actorName}->Avatars"."actorId" ` +
38 `AND "${actorName}->Avatars"."type" = ${ActorImageType.AVATAR}`
39 }
40
41 private buildInnerQuery () {
42 this.innerQuery = `${this.getInnerSelect()} ` +
43 `FROM "actorFollow" AS "ActorFollowModel" ` +
44 `${this.getJoins()} ` +
45 `${this.getServerJoin('ActorFollowing')} ` +
46 `${this.getServerJoin('ActorFollower')} ` +
47 `${this.getWhere()} ` +
48 `${this.getOrder()} ` +
49 `LIMIT :limit OFFSET :offset `
50
51 this.replacements.limit = this.options.count
52 this.replacements.offset = this.options.start
53 }
54
55 protected buildListQuery () {
56 this.buildInnerQuery()
57
58 this.query = `${this.getSelect()} ` +
59 `FROM (${this.innerQuery}) AS "ActorFollowModel" ` +
60 `${this.getAvatarsJoin('ActorFollower')} ` +
61 `${this.getAvatarsJoin('ActorFollowing')} ` +
62 `${this.getOrder()}`
63 }
64
65 protected buildCountQuery () {
66 this.query = `SELECT COUNT(*) AS "total" ` +
67 `FROM "actorFollow" AS "ActorFollowModel" ` +
68 `${this.getJoins()} ` +
69 `${this.getServerJoin('ActorFollowing')} ` +
70 `${this.getServerJoin('ActorFollower')} ` +
71 `${this.getWhere()}`
72 }
73
74 private getInnerSelect () {
75 return this.buildSelect([
76 this.tableAttributes.getFollowAttributes(),
77 this.tableAttributes.getActorAttributes('ActorFollower'),
78 this.tableAttributes.getActorAttributes('ActorFollowing'),
79 this.tableAttributes.getServerAttributes('ActorFollower'),
80 this.tableAttributes.getServerAttributes('ActorFollowing')
81 ])
82 }
83
84 private getSelect () {
85 return this.buildSelect([
86 '"ActorFollowModel".*',
87 this.tableAttributes.getAvatarAttributes('ActorFollower'),
88 this.tableAttributes.getAvatarAttributes('ActorFollowing')
89 ])
90 }
91
92 private getOrder () {
93 const orders = getInstanceFollowsSort(this.options.sort)
94
95 return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ')
96 }
97}
diff --git a/server/models/shared/abstract-run-query.ts b/server/models/shared/abstract-run-query.ts
index c39b7bcfe..f1182c7be 100644
--- a/server/models/shared/abstract-run-query.ts
+++ b/server/models/shared/abstract-run-query.ts
@@ -25,4 +25,8 @@ export class AbstractRunQuery {
25 25
26 return this.sequelize.query<any>(this.query, queryOptions) 26 return this.sequelize.query<any>(this.query, queryOptions)
27 } 27 }
28
29 protected buildSelect (entities: string[]) {
30 return `SELECT ${entities.join(', ')} `
31 }
28} 32}
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 70bfbdb8b..b57290aff 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -87,12 +87,12 @@ function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'i
87 return [ firstSort, lastSort ] 87 return [ firstSort, lastSort ]
88} 88}
89 89
90function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 90function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
91 const { direction, field } = buildDirectionAndField(value) 91 const { direction, field } = buildDirectionAndField(value)
92 92
93 if (field === 'redundancyAllowed') { 93 if (field === 'redundancyAllowed') {
94 return [ 94 return [
95 [ 'ActorFollowing', 'Server', 'redundancyAllowed', direction ], 95 [ 'ActorFollowing.Server.redundancyAllowed', direction ],
96 lastSort 96 lastSort
97 ] 97 ]
98 } 98 }
@@ -197,6 +197,12 @@ function parseAggregateResult (result: any) {
197 return total 197 return total
198} 198}
199 199
200function parseRowCountResult (result: any) {
201 if (result.length !== 0) return result[0].total
202
203 return 0
204}
205
200function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { 206function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
201 return stringArr.map(t => { 207 return stringArr.map(t => {
202 return t === null 208 return t === null
@@ -263,10 +269,11 @@ export {
263 buildWhereIdOrUUID, 269 buildWhereIdOrUUID,
264 isOutdated, 270 isOutdated,
265 parseAggregateResult, 271 parseAggregateResult,
266 getFollowsSort, 272 getInstanceFollowsSort,
267 buildDirectionAndField, 273 buildDirectionAndField,
268 createSafeIn, 274 createSafeIn,
269 searchAttribute 275 searchAttribute,
276 parseRowCountResult
270} 277}
271 278
272// --------------------------------------------------------------------------- 279// ---------------------------------------------------------------------------
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts
index 09cb791db..8692a436a 100644
--- a/server/models/video/sql/video/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/video/videos-id-list-query-builder.ts
@@ -2,7 +2,7 @@ import { Sequelize } from 'sequelize'
2import validator from 'validator' 2import validator from 'validator'
3import { exists } from '@server/helpers/custom-validators/misc' 3import { exists } from '@server/helpers/custom-validators/misc'
4import { WEBSERVER } from '@server/initializers/constants' 4import { WEBSERVER } from '@server/initializers/constants'
5import { buildDirectionAndField, createSafeIn } from '@server/models/utils' 5import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils'
6import { MUserAccountId, MUserId } from '@server/types/models' 6import { MUserAccountId, MUserId } from '@server/types/models'
7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' 7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
8import { AbstractRunQuery } from '../../../shared/abstract-run-query' 8import { AbstractRunQuery } from '../../../shared/abstract-run-query'
@@ -105,7 +105,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
105 countVideoIds (countOptions: BuildVideosListQueryOptions): Promise<number> { 105 countVideoIds (countOptions: BuildVideosListQueryOptions): Promise<number> {
106 this.buildIdsListQuery(countOptions) 106 this.buildIdsListQuery(countOptions)
107 107
108 return this.runQuery().then(rows => rows.length !== 0 ? rows[0].total : 0) 108 return this.runQuery().then(rows => parseRowCountResult(rows))
109 } 109 }
110 110
111 getQuery (options: BuildVideosListQueryOptions) { 111 getQuery (options: BuildVideosListQueryOptions) {
diff --git a/server/tests/api/redundancy/manage-redundancy.ts b/server/tests/api/redundancy/manage-redundancy.ts
index cbf3106bd..d415ac3bf 100644
--- a/server/tests/api/redundancy/manage-redundancy.ts
+++ b/server/tests/api/redundancy/manage-redundancy.ts
@@ -69,6 +69,7 @@ describe('Test manage videos redundancy', function () {
69 69
70 // Server 1 and server 2 follow each other 70 // Server 1 and server 2 follow each other
71 await doubleFollow(servers[0], servers[1]) 71 await doubleFollow(servers[0], servers[1])
72 await doubleFollow(servers[0], servers[2])
72 await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true }) 73 await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
73 74
74 await waitJobs(servers) 75 await waitJobs(servers)
@@ -83,6 +84,16 @@ describe('Test manage videos redundancy', function () {
83 } 84 }
84 }) 85 })
85 86
87 it('Should correctly list followings by redundancy', async function () {
88 const body = await servers[0].follows.getFollowings({ sort: '-redundancyAllowed' })
89
90 expect(body.total).to.equal(2)
91 expect(body.data).to.have.lengthOf(2)
92
93 expect(body.data[0].following.host).to.equal(servers[1].host)
94 expect(body.data[1].following.host).to.equal(servers[2].host)
95 })
96
86 it('Should not have "remote-videos" redundancies on server 2', async function () { 97 it('Should not have "remote-videos" redundancies on server 2', async function () {
87 this.timeout(120000) 98 this.timeout(120000)
88 99