aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/activitypub')
-rw-r--r--server/models/activitypub/actor-follow.ts260
-rw-r--r--server/models/activitypub/actor.ts165
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 @@
1import * as Bluebird from 'bluebird'
2import { values } from 'lodash'
3import * as Sequelize from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { FollowState } from '../../../shared/models/actors'
6import { FOLLOW_STATES } from '../../initializers/constants'
7import { ServerModel } from '../server/server'
8import { getSort } from '../utils'
9import { 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})
26export 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 @@
1import { values } from 'lodash'
1import { join } from 'path' 2import { join } from 'path'
2import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
3import { 4import {
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'
21import { ActivityPubActorType } from '../../../shared/models/activitypub'
7import { Avatar } from '../../../shared/models/avatars/avatar.model' 22import { Avatar } from '../../../shared/models/avatars/avatar.model'
8import { activityPubContextify } from '../../helpers' 23import { activityPubContextify } from '../../helpers'
9import { 24import {
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'
16import { isUserUsernameValid } from '../../helpers/custom-validators/users' 32import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
17import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 33import { AccountModel } from '../account/account'
18import { AccountFollowModel } from '../account/account-follow'
19import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
20import { ServerModel } from '../server/server' 35import { ServerModel } from '../server/server'
21import { throwIfNotValid } from '../utils' 36import { throwIfNotValid } from '../utils'
37import { VideoChannelModel } from '../video/video-channel'
38import { ActorFollowModel } from './actor-follow'
22 39
40enum 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})
26export class ActorModel extends Model<ActorModel> { 67export 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 ],