diff options
Diffstat (limited to 'server/models/activitypub')
-rw-r--r-- | server/models/activitypub/actor-follow.ts | 260 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 165 |
2 files changed, 402 insertions, 23 deletions
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts new file mode 100644 index 000000000..4cba05e95 --- /dev/null +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -0,0 +1,260 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { values } from 'lodash' | ||
3 | import * as Sequelize from 'sequelize' | ||
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
5 | import { FollowState } from '../../../shared/models/actors' | ||
6 | import { FOLLOW_STATES } from '../../initializers/constants' | ||
7 | import { ServerModel } from '../server/server' | ||
8 | import { getSort } from '../utils' | ||
9 | import { ActorModel } from './actor' | ||
10 | |||
11 | @Table({ | ||
12 | tableName: 'actorFollow', | ||
13 | indexes: [ | ||
14 | { | ||
15 | fields: [ 'actorId' ] | ||
16 | }, | ||
17 | { | ||
18 | fields: [ 'targetActorId' ] | ||
19 | }, | ||
20 | { | ||
21 | fields: [ 'actorId', 'targetActorId' ], | ||
22 | unique: true | ||
23 | } | ||
24 | ] | ||
25 | }) | ||
26 | export class ActorFollowModel extends Model<ActorFollowModel> { | ||
27 | |||
28 | @AllowNull(false) | ||
29 | @Column(DataType.ENUM(values(FOLLOW_STATES))) | ||
30 | state: FollowState | ||
31 | |||
32 | @CreatedAt | ||
33 | createdAt: Date | ||
34 | |||
35 | @UpdatedAt | ||
36 | updatedAt: Date | ||
37 | |||
38 | @ForeignKey(() => ActorModel) | ||
39 | @Column | ||
40 | actorId: number | ||
41 | |||
42 | @BelongsTo(() => ActorModel, { | ||
43 | foreignKey: { | ||
44 | name: 'actorId', | ||
45 | allowNull: false | ||
46 | }, | ||
47 | as: 'ActorFollower', | ||
48 | onDelete: 'CASCADE' | ||
49 | }) | ||
50 | ActorFollower: ActorModel | ||
51 | |||
52 | @ForeignKey(() => ActorModel) | ||
53 | @Column | ||
54 | targetActorId: number | ||
55 | |||
56 | @BelongsTo(() => ActorModel, { | ||
57 | foreignKey: { | ||
58 | name: 'targetActorId', | ||
59 | allowNull: false | ||
60 | }, | ||
61 | as: 'ActorFollowing', | ||
62 | onDelete: 'CASCADE' | ||
63 | }) | ||
64 | ActorFollowing: ActorModel | ||
65 | |||
66 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { | ||
67 | const query = { | ||
68 | where: { | ||
69 | actorId, | ||
70 | targetActorId: targetActorId | ||
71 | }, | ||
72 | include: [ | ||
73 | { | ||
74 | model: ActorModel, | ||
75 | required: true, | ||
76 | as: 'ActorFollower' | ||
77 | }, | ||
78 | { | ||
79 | model: ActorModel, | ||
80 | required: true, | ||
81 | as: 'ActorFollowing' | ||
82 | } | ||
83 | ], | ||
84 | transaction: t | ||
85 | } | ||
86 | |||
87 | return ActorFollowModel.findOne(query) | ||
88 | } | ||
89 | |||
90 | static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) { | ||
91 | const query = { | ||
92 | where: { | ||
93 | actorId | ||
94 | }, | ||
95 | include: [ | ||
96 | { | ||
97 | model: ActorModel, | ||
98 | required: true, | ||
99 | as: 'ActorFollower' | ||
100 | }, | ||
101 | { | ||
102 | model: ActorModel, | ||
103 | required: true, | ||
104 | as: 'ActorFollowing', | ||
105 | include: [ | ||
106 | { | ||
107 | model: ServerModel, | ||
108 | required: true, | ||
109 | where: { | ||
110 | host: targetHost | ||
111 | } | ||
112 | } | ||
113 | ] | ||
114 | } | ||
115 | ], | ||
116 | transaction: t | ||
117 | } | ||
118 | |||
119 | return ActorFollowModel.findOne(query) | ||
120 | } | ||
121 | |||
122 | static listFollowingForApi (id: number, start: number, count: number, sort: string) { | ||
123 | const query = { | ||
124 | distinct: true, | ||
125 | offset: start, | ||
126 | limit: count, | ||
127 | order: [ getSort(sort) ], | ||
128 | include: [ | ||
129 | { | ||
130 | model: ActorModel, | ||
131 | required: true, | ||
132 | as: 'ActorFollower', | ||
133 | where: { | ||
134 | id | ||
135 | } | ||
136 | }, | ||
137 | { | ||
138 | model: ActorModel, | ||
139 | as: 'ActorFollowing', | ||
140 | required: true, | ||
141 | include: [ ServerModel ] | ||
142 | } | ||
143 | ] | ||
144 | } | ||
145 | |||
146 | return ActorFollowModel.findAndCountAll(query) | ||
147 | .then(({ rows, count }) => { | ||
148 | return { | ||
149 | data: rows, | ||
150 | total: count | ||
151 | } | ||
152 | }) | ||
153 | } | ||
154 | |||
155 | static listFollowersForApi (id: number, start: number, count: number, sort: string) { | ||
156 | const query = { | ||
157 | distinct: true, | ||
158 | offset: start, | ||
159 | limit: count, | ||
160 | order: [ getSort(sort) ], | ||
161 | include: [ | ||
162 | { | ||
163 | model: ActorModel, | ||
164 | required: true, | ||
165 | as: 'ActorFollower', | ||
166 | include: [ ServerModel ] | ||
167 | }, | ||
168 | { | ||
169 | model: ActorModel, | ||
170 | as: 'ActorFollowing', | ||
171 | required: true, | ||
172 | where: { | ||
173 | id | ||
174 | } | ||
175 | } | ||
176 | ] | ||
177 | } | ||
178 | |||
179 | return ActorFollowModel.findAndCountAll(query) | ||
180 | .then(({ rows, count }) => { | ||
181 | return { | ||
182 | data: rows, | ||
183 | total: count | ||
184 | } | ||
185 | }) | ||
186 | } | ||
187 | |||
188 | static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { | ||
189 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) | ||
190 | } | ||
191 | |||
192 | static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) { | ||
193 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, undefined, undefined, 'sharedInboxUrl') | ||
194 | } | ||
195 | |||
196 | static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { | ||
197 | return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) | ||
198 | } | ||
199 | |||
200 | private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following', | ||
201 | actorIds: number[], | ||
202 | t: Sequelize.Transaction, | ||
203 | start?: number, | ||
204 | count?: number, | ||
205 | columnUrl = 'url') { | ||
206 | let firstJoin: string | ||
207 | let secondJoin: string | ||
208 | |||
209 | if (type === 'followers') { | ||
210 | firstJoin = 'targetActorId' | ||
211 | secondJoin = 'actorId' | ||
212 | } else { | ||
213 | firstJoin = 'actorId' | ||
214 | secondJoin = 'targetActorId' | ||
215 | } | ||
216 | |||
217 | const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] | ||
218 | const tasks: Bluebird<any>[] = [] | ||
219 | |||
220 | for (const selection of selections) { | ||
221 | let query = 'SELECT ' + selection + ' FROM "actor" ' + | ||
222 | 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + | ||
223 | 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + | ||
224 | 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' ' | ||
225 | |||
226 | if (count !== undefined) query += 'LIMIT ' + count | ||
227 | if (start !== undefined) query += ' OFFSET ' + start | ||
228 | |||
229 | const options = { | ||
230 | bind: { actorIds }, | ||
231 | type: Sequelize.QueryTypes.SELECT, | ||
232 | transaction: t | ||
233 | } | ||
234 | tasks.push(ActorFollowModel.sequelize.query(query, options)) | ||
235 | } | ||
236 | |||
237 | const [ followers, [ { total } ] ] = await | ||
238 | Promise.all(tasks) | ||
239 | const urls: string[] = followers.map(f => f.url) | ||
240 | |||
241 | return { | ||
242 | data: urls, | ||
243 | total: parseInt(total, 10) | ||
244 | } | ||
245 | } | ||
246 | |||
247 | toFormattedJSON () { | ||
248 | const follower = this.ActorFollower.toFormattedJSON() | ||
249 | const following = this.ActorFollowing.toFormattedJSON() | ||
250 | |||
251 | return { | ||
252 | id: this.id, | ||
253 | follower, | ||
254 | following, | ||
255 | state: this.state, | ||
256 | createdAt: this.createdAt, | ||
257 | updatedAt: this.updatedAt | ||
258 | } | ||
259 | } | ||
260 | } | ||
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 4cae6a6ec..ecaa43dcf 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -1,38 +1,83 @@ | |||
1 | import { values } from 'lodash' | ||
1 | import { join } from 'path' | 2 | import { join } from 'path' |
2 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
3 | import { | 4 | import { |
4 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table, | 5 | AllowNull, |
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | DataType, | ||
10 | Default, | ||
11 | ForeignKey, | ||
12 | HasMany, | ||
13 | HasOne, | ||
14 | Is, | ||
15 | IsUUID, | ||
16 | Model, | ||
17 | Scopes, | ||
18 | Table, | ||
5 | UpdatedAt | 19 | UpdatedAt |
6 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
21 | import { ActivityPubActorType } from '../../../shared/models/activitypub' | ||
7 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 22 | import { Avatar } from '../../../shared/models/avatars/avatar.model' |
8 | import { activityPubContextify } from '../../helpers' | 23 | import { activityPubContextify } from '../../helpers' |
9 | import { | 24 | import { |
10 | isActivityPubUrlValid, | 25 | isActivityPubUrlValid, |
11 | isActorFollowersCountValid, | 26 | isActorFollowersCountValid, |
12 | isActorFollowingCountValid, isActorPreferredUsernameValid, | 27 | isActorFollowingCountValid, |
28 | isActorNameValid, | ||
13 | isActorPrivateKeyValid, | 29 | isActorPrivateKeyValid, |
14 | isActorPublicKeyValid | 30 | isActorPublicKeyValid |
15 | } from '../../helpers/custom-validators/activitypub' | 31 | } from '../../helpers/custom-validators/activitypub' |
16 | import { isUserUsernameValid } from '../../helpers/custom-validators/users' | 32 | import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
17 | import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' | 33 | import { AccountModel } from '../account/account' |
18 | import { AccountFollowModel } from '../account/account-follow' | ||
19 | import { AvatarModel } from '../avatar/avatar' | 34 | import { AvatarModel } from '../avatar/avatar' |
20 | import { ServerModel } from '../server/server' | 35 | import { ServerModel } from '../server/server' |
21 | import { throwIfNotValid } from '../utils' | 36 | import { throwIfNotValid } from '../utils' |
37 | import { VideoChannelModel } from '../video/video-channel' | ||
38 | import { ActorFollowModel } from './actor-follow' | ||
22 | 39 | ||
40 | enum ScopeNames { | ||
41 | FULL = 'FULL' | ||
42 | } | ||
43 | |||
44 | @Scopes({ | ||
45 | [ScopeNames.FULL]: { | ||
46 | include: [ | ||
47 | { | ||
48 | model: () => AccountModel, | ||
49 | required: false | ||
50 | }, | ||
51 | { | ||
52 | model: () => VideoChannelModel, | ||
53 | required: false | ||
54 | } | ||
55 | ] | ||
56 | } | ||
57 | }) | ||
23 | @Table({ | 58 | @Table({ |
24 | tableName: 'actor' | 59 | tableName: 'actor', |
60 | indexes: [ | ||
61 | { | ||
62 | fields: [ 'name', 'serverId' ], | ||
63 | unique: true | ||
64 | } | ||
65 | ] | ||
25 | }) | 66 | }) |
26 | export class ActorModel extends Model<ActorModel> { | 67 | export class ActorModel extends Model<ActorModel> { |
27 | 68 | ||
28 | @AllowNull(false) | 69 | @AllowNull(false) |
70 | @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES))) | ||
71 | type: ActivityPubActorType | ||
72 | |||
73 | @AllowNull(false) | ||
29 | @Default(DataType.UUIDV4) | 74 | @Default(DataType.UUIDV4) |
30 | @IsUUID(4) | 75 | @IsUUID(4) |
31 | @Column(DataType.UUID) | 76 | @Column(DataType.UUID) |
32 | uuid: string | 77 | uuid: string |
33 | 78 | ||
34 | @AllowNull(false) | 79 | @AllowNull(false) |
35 | @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name')) | 80 | @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name')) |
36 | @Column | 81 | @Column |
37 | name: string | 82 | name: string |
38 | 83 | ||
@@ -104,24 +149,24 @@ export class ActorModel extends Model<ActorModel> { | |||
104 | }) | 149 | }) |
105 | Avatar: AvatarModel | 150 | Avatar: AvatarModel |
106 | 151 | ||
107 | @HasMany(() => AccountFollowModel, { | 152 | @HasMany(() => ActorFollowModel, { |
108 | foreignKey: { | 153 | foreignKey: { |
109 | name: 'accountId', | 154 | name: 'actorId', |
110 | allowNull: false | 155 | allowNull: false |
111 | }, | 156 | }, |
112 | onDelete: 'cascade' | 157 | onDelete: 'cascade' |
113 | }) | 158 | }) |
114 | AccountFollowing: AccountFollowModel[] | 159 | AccountFollowing: ActorFollowModel[] |
115 | 160 | ||
116 | @HasMany(() => AccountFollowModel, { | 161 | @HasMany(() => ActorFollowModel, { |
117 | foreignKey: { | 162 | foreignKey: { |
118 | name: 'targetAccountId', | 163 | name: 'targetActorId', |
119 | allowNull: false | 164 | allowNull: false |
120 | }, | 165 | }, |
121 | as: 'followers', | 166 | as: 'followers', |
122 | onDelete: 'cascade' | 167 | onDelete: 'cascade' |
123 | }) | 168 | }) |
124 | AccountFollowers: AccountFollowModel[] | 169 | AccountFollowers: ActorFollowModel[] |
125 | 170 | ||
126 | @ForeignKey(() => ServerModel) | 171 | @ForeignKey(() => ServerModel) |
127 | @Column | 172 | @Column |
@@ -135,6 +180,36 @@ export class ActorModel extends Model<ActorModel> { | |||
135 | }) | 180 | }) |
136 | Server: ServerModel | 181 | Server: ServerModel |
137 | 182 | ||
183 | @HasOne(() => AccountModel, { | ||
184 | foreignKey: { | ||
185 | allowNull: true | ||
186 | }, | ||
187 | onDelete: 'cascade' | ||
188 | }) | ||
189 | Account: AccountModel | ||
190 | |||
191 | @HasOne(() => VideoChannelModel, { | ||
192 | foreignKey: { | ||
193 | allowNull: true | ||
194 | }, | ||
195 | onDelete: 'cascade' | ||
196 | }) | ||
197 | VideoChannel: VideoChannelModel | ||
198 | |||
199 | static load (id: number) { | ||
200 | return ActorModel.scope(ScopeNames.FULL).findById(id) | ||
201 | } | ||
202 | |||
203 | static loadByUUID (uuid: string) { | ||
204 | const query = { | ||
205 | where: { | ||
206 | uuid | ||
207 | } | ||
208 | } | ||
209 | |||
210 | return ActorModel.scope(ScopeNames.FULL).findOne(query) | ||
211 | } | ||
212 | |||
138 | static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { | 213 | static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { |
139 | const query = { | 214 | const query = { |
140 | where: { | 215 | where: { |
@@ -145,7 +220,48 @@ export class ActorModel extends Model<ActorModel> { | |||
145 | transaction | 220 | transaction |
146 | } | 221 | } |
147 | 222 | ||
148 | return ActorModel.findAll(query) | 223 | return ActorModel.scope(ScopeNames.FULL).findAll(query) |
224 | } | ||
225 | |||
226 | static loadLocalByName (name: string) { | ||
227 | const query = { | ||
228 | where: { | ||
229 | name, | ||
230 | serverId: null | ||
231 | } | ||
232 | } | ||
233 | |||
234 | return ActorModel.scope(ScopeNames.FULL).findOne(query) | ||
235 | } | ||
236 | |||
237 | static loadByNameAndHost (name: string, host: string) { | ||
238 | const query = { | ||
239 | where: { | ||
240 | name | ||
241 | }, | ||
242 | include: [ | ||
243 | { | ||
244 | model: ServerModel, | ||
245 | required: true, | ||
246 | where: { | ||
247 | host | ||
248 | } | ||
249 | } | ||
250 | ] | ||
251 | } | ||
252 | |||
253 | return ActorModel.scope(ScopeNames.FULL).findOne(query) | ||
254 | } | ||
255 | |||
256 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | ||
257 | const query = { | ||
258 | where: { | ||
259 | url | ||
260 | }, | ||
261 | transaction | ||
262 | } | ||
263 | |||
264 | return ActorModel.scope(ScopeNames.FULL).findOne(query) | ||
149 | } | 265 | } |
150 | 266 | ||
151 | toFormattedJSON () { | 267 | toFormattedJSON () { |
@@ -167,6 +283,7 @@ export class ActorModel extends Model<ActorModel> { | |||
167 | 283 | ||
168 | return { | 284 | return { |
169 | id: this.id, | 285 | id: this.id, |
286 | uuid: this.uuid, | ||
170 | host, | 287 | host, |
171 | score, | 288 | score, |
172 | followingCount: this.followingCount, | 289 | followingCount: this.followingCount, |
@@ -175,28 +292,30 @@ export class ActorModel extends Model<ActorModel> { | |||
175 | } | 292 | } |
176 | } | 293 | } |
177 | 294 | ||
178 | toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') { | 295 | toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') { |
179 | let activityPubType | 296 | let activityPubType |
180 | if (type === 'Account') { | 297 | if (type === 'Account') { |
181 | activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' | 298 | activityPubType = 'Person' as 'Person' |
299 | } else if (type === 'Application') { | ||
300 | activityPubType = 'Application' as 'Application' | ||
182 | } else { // VideoChannel | 301 | } else { // VideoChannel |
183 | activityPubType = 'Group' | 302 | activityPubType = 'Group' as 'Group' |
184 | } | 303 | } |
185 | 304 | ||
186 | const json = { | 305 | const json = { |
187 | type, | 306 | type: activityPubType, |
188 | id: this.url, | 307 | id: this.url, |
189 | following: this.getFollowingUrl(), | 308 | following: this.getFollowingUrl(), |
190 | followers: this.getFollowersUrl(), | 309 | followers: this.getFollowersUrl(), |
191 | inbox: this.inboxUrl, | 310 | inbox: this.inboxUrl, |
192 | outbox: this.outboxUrl, | 311 | outbox: this.outboxUrl, |
193 | preferredUsername: name, | 312 | preferredUsername, |
194 | url: this.url, | 313 | url: this.url, |
195 | name, | 314 | name: this.name, |
196 | endpoints: { | 315 | endpoints: { |
197 | sharedInbox: this.sharedInboxUrl | 316 | sharedInbox: this.sharedInboxUrl |
198 | }, | 317 | }, |
199 | uuid, | 318 | uuid: this.uuid, |
200 | publicKey: { | 319 | publicKey: { |
201 | id: this.getPublicKeyUrl(), | 320 | id: this.getPublicKeyUrl(), |
202 | owner: this.url, | 321 | owner: this.url, |
@@ -212,11 +331,11 @@ export class ActorModel extends Model<ActorModel> { | |||
212 | attributes: [ 'sharedInboxUrl' ], | 331 | attributes: [ 'sharedInboxUrl' ], |
213 | include: [ | 332 | include: [ |
214 | { | 333 | { |
215 | model: AccountFollowModel, | 334 | model: ActorFollowModel, |
216 | required: true, | 335 | required: true, |
217 | as: 'followers', | 336 | as: 'followers', |
218 | where: { | 337 | where: { |
219 | targetAccountId: this.id | 338 | targetActorId: this.id |
220 | } | 339 | } |
221 | } | 340 | } |
222 | ], | 341 | ], |