aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/actor
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-05-05 10:29:35 +0200
committerChocobozzz <me@florianbigard.com>2022-05-05 10:29:35 +0200
commitbae616273d455d225d131eb17c56db6c20a0b6b3 (patch)
treeed2174122d092320cea34c4ae522da6f2131f968 /server/models/actor
parentd493e2d4bf0ace177d15e9173d1c02df8c100558 (diff)
downloadPeerTube-bae616273d455d225d131eb17c56db6c20a0b6b3.tar.gz
PeerTube-bae616273d455d225d131eb17c56db6c20a0b6b3.tar.zst
PeerTube-bae616273d455d225d131eb17c56db6c20a0b6b3.zip
Convert followers/following in raw SQL queries
Prevent weird bug in SQL generation
Diffstat (limited to 'server/models/actor')
-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
5 files changed, 307 insertions, 136 deletions
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}