diff options
Diffstat (limited to 'server/models/actor/sql')
4 files changed, 297 insertions, 0 deletions
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 | } | ||