diff options
-rw-r--r-- | server/controllers/api/server/follows.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-undo.ts | 2 | ||||
-rw-r--r-- | server/models/actor/actor-follow.ts | 146 | ||||
-rw-r--r-- | server/models/actor/sql/instance-list-followers-query-builder.ts | 69 | ||||
-rw-r--r-- | server/models/actor/sql/instance-list-following-query-builder.ts | 69 | ||||
-rw-r--r-- | server/models/actor/sql/shared/actor-follow-table-attributes.ts | 62 | ||||
-rw-r--r-- | server/models/actor/sql/shared/instance-list-follows-query-builder.ts | 97 | ||||
-rw-r--r-- | server/models/shared/abstract-run-query.ts | 4 | ||||
-rw-r--r-- | server/models/utils.ts | 15 | ||||
-rw-r--r-- | server/models/video/sql/video/videos-id-list-query-builder.ts | 4 | ||||
-rw-r--r-- | server/tests/api/redundancy/manage-redundancy.ts | 11 |
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 { | |||
99 | async function listFollowing (req: express.Request, res: express.Response) { | 99 | async 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 @@ | |||
1 | import { difference, values } from 'lodash' | 1 | import { difference, values } from 'lodash' |
2 | import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' | 2 | import { Includeable, IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize' |
3 | import { | 3 | import { |
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' |
33 | import { ActivityPubActorType } from '@shared/models' | ||
34 | import { AttributesOnly } from '@shared/typescript-utils' | 33 | import { AttributesOnly } from '@shared/typescript-utils' |
35 | import { FollowState } from '../../../shared/models/actors' | 34 | import { FollowState } from '../../../shared/models/actors' |
36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 35 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
@@ -39,9 +38,11 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM | |||
39 | import { AccountModel } from '../account/account' | 38 | import { AccountModel } from '../account/account' |
40 | import { ServerModel } from '../server/server' | 39 | import { ServerModel } from '../server/server' |
41 | import { doesExist } from '../shared/query' | 40 | import { doesExist } from '../shared/query' |
42 | import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' | 41 | import { createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils' |
43 | import { VideoChannelModel } from '../video/video-channel' | 42 | import { VideoChannelModel } from '../video/video-channel' |
44 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | ||
45 | import { 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 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { ModelBuilder } from '@server/models/shared' | ||
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | ||
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | ||
7 | |||
8 | export 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 | |||
18 | export 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 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { ModelBuilder } from '@server/models/shared' | ||
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | ||
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | ||
7 | |||
8 | export 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 | |||
18 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { AbstractRunQuery } from '@server/models/shared' | ||
3 | import { getInstanceFollowsSort } from '@server/models/utils' | ||
4 | import { ActorImageType } from '@shared/models' | ||
5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' | ||
6 | |||
7 | type BaseOptions = { | ||
8 | sort: string | ||
9 | count: number | ||
10 | start: number | ||
11 | } | ||
12 | |||
13 | export 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 | ||
90 | function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | 90 | function 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 | ||
200 | function parseRowCountResult (result: any) { | ||
201 | if (result.length !== 0) return result[0].total | ||
202 | |||
203 | return 0 | ||
204 | } | ||
205 | |||
200 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { | 206 | function 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' | |||
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { exists } from '@server/helpers/custom-validators/misc' | 3 | import { exists } from '@server/helpers/custom-validators/misc' |
4 | import { WEBSERVER } from '@server/initializers/constants' | 4 | import { WEBSERVER } from '@server/initializers/constants' |
5 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | 5 | import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' |
6 | import { MUserAccountId, MUserId } from '@server/types/models' | 6 | import { MUserAccountId, MUserId } from '@server/types/models' |
7 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' | 7 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' |
8 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' | 8 | import { 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 | ||