actor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import { values } from 'lodash'
  2. import { extname } from 'path'
  3. import * as Sequelize from 'sequelize'
  4. import {
  5. AllowNull,
  6. BelongsTo,
  7. Column,
  8. CreatedAt,
  9. DataType,
  10. Default,
  11. DefaultScope,
  12. ForeignKey,
  13. HasMany,
  14. HasOne,
  15. Is,
  16. IsUUID,
  17. Model,
  18. Scopes,
  19. Table,
  20. UpdatedAt
  21. } from 'sequelize-typescript'
  22. import { ActivityPubActorType } from '../../../shared/models/activitypub'
  23. import { Avatar } from '../../../shared/models/avatars/avatar.model'
  24. import { activityPubContextify } from '../../helpers/activitypub'
  25. import {
  26. isActorFollowersCountValid,
  27. isActorFollowingCountValid,
  28. isActorPreferredUsernameValid,
  29. isActorPrivateKeyValid,
  30. isActorPublicKeyValid
  31. } from '../../helpers/custom-validators/activitypub/actor'
  32. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  33. import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
  34. import { AccountModel } from '../account/account'
  35. import { AvatarModel } from '../avatar/avatar'
  36. import { ServerModel } from '../server/server'
  37. import { throwIfNotValid } from '../utils'
  38. import { VideoChannelModel } from '../video/video-channel'
  39. import { ActorFollowModel } from './actor-follow'
  40. import { VideoModel } from '../video/video'
  41. enum ScopeNames {
  42. FULL = 'FULL'
  43. }
  44. export const unusedActorAttributesForAPI = [
  45. 'publicKey',
  46. 'privateKey',
  47. 'inboxUrl',
  48. 'outboxUrl',
  49. 'sharedInboxUrl',
  50. 'followersUrl',
  51. 'followingUrl',
  52. 'url',
  53. 'createdAt',
  54. 'updatedAt'
  55. ]
  56. @DefaultScope({
  57. include: [
  58. {
  59. model: () => ServerModel,
  60. required: false
  61. },
  62. {
  63. model: () => AvatarModel,
  64. required: false
  65. }
  66. ]
  67. })
  68. @Scopes({
  69. [ScopeNames.FULL]: {
  70. include: [
  71. {
  72. model: () => AccountModel.unscoped(),
  73. required: false
  74. },
  75. {
  76. model: () => VideoChannelModel.unscoped(),
  77. required: false,
  78. include: [
  79. {
  80. model: () => AccountModel,
  81. required: true
  82. }
  83. ]
  84. },
  85. {
  86. model: () => ServerModel,
  87. required: false
  88. },
  89. {
  90. model: () => AvatarModel,
  91. required: false
  92. }
  93. ]
  94. }
  95. })
  96. @Table({
  97. tableName: 'actor',
  98. indexes: [
  99. {
  100. fields: [ 'url' ],
  101. unique: true
  102. },
  103. {
  104. fields: [ 'preferredUsername', 'serverId' ],
  105. unique: true
  106. },
  107. {
  108. fields: [ 'inboxUrl', 'sharedInboxUrl' ]
  109. },
  110. {
  111. fields: [ 'sharedInboxUrl' ]
  112. },
  113. {
  114. fields: [ 'serverId' ]
  115. },
  116. {
  117. fields: [ 'avatarId' ]
  118. },
  119. {
  120. fields: [ 'uuid' ],
  121. unique: true
  122. },
  123. {
  124. fields: [ 'followersUrl' ]
  125. }
  126. ]
  127. })
  128. export class ActorModel extends Model<ActorModel> {
  129. @AllowNull(false)
  130. @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
  131. type: ActivityPubActorType
  132. @AllowNull(false)
  133. @Default(DataType.UUIDV4)
  134. @IsUUID(4)
  135. @Column(DataType.UUID)
  136. uuid: string
  137. @AllowNull(false)
  138. @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
  139. @Column
  140. preferredUsername: string
  141. @AllowNull(false)
  142. @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  143. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  144. url: string
  145. @AllowNull(true)
  146. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key'))
  147. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
  148. publicKey: string
  149. @AllowNull(true)
  150. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key'))
  151. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
  152. privateKey: string
  153. @AllowNull(false)
  154. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
  155. @Column
  156. followersCount: number
  157. @AllowNull(false)
  158. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
  159. @Column
  160. followingCount: number
  161. @AllowNull(false)
  162. @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
  163. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  164. inboxUrl: string
  165. @AllowNull(false)
  166. @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
  167. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  168. outboxUrl: string
  169. @AllowNull(false)
  170. @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
  171. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  172. sharedInboxUrl: string
  173. @AllowNull(false)
  174. @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
  175. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  176. followersUrl: string
  177. @AllowNull(false)
  178. @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
  179. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  180. followingUrl: string
  181. @CreatedAt
  182. createdAt: Date
  183. @UpdatedAt
  184. updatedAt: Date
  185. @ForeignKey(() => AvatarModel)
  186. @Column
  187. avatarId: number
  188. @BelongsTo(() => AvatarModel, {
  189. foreignKey: {
  190. allowNull: true
  191. },
  192. onDelete: 'set null',
  193. hooks: true
  194. })
  195. Avatar: AvatarModel
  196. @HasMany(() => ActorFollowModel, {
  197. foreignKey: {
  198. name: 'actorId',
  199. allowNull: false
  200. },
  201. onDelete: 'cascade'
  202. })
  203. ActorFollowing: ActorFollowModel[]
  204. @HasMany(() => ActorFollowModel, {
  205. foreignKey: {
  206. name: 'targetActorId',
  207. allowNull: false
  208. },
  209. as: 'ActorFollowers',
  210. onDelete: 'cascade'
  211. })
  212. ActorFollowers: ActorFollowModel[]
  213. @ForeignKey(() => ServerModel)
  214. @Column
  215. serverId: number
  216. @BelongsTo(() => ServerModel, {
  217. foreignKey: {
  218. allowNull: true
  219. },
  220. onDelete: 'cascade'
  221. })
  222. Server: ServerModel
  223. @HasOne(() => AccountModel, {
  224. foreignKey: {
  225. allowNull: true
  226. },
  227. onDelete: 'cascade',
  228. hooks: true
  229. })
  230. Account: AccountModel
  231. @HasOne(() => VideoChannelModel, {
  232. foreignKey: {
  233. allowNull: true
  234. },
  235. onDelete: 'cascade',
  236. hooks: true
  237. })
  238. VideoChannel: VideoChannelModel
  239. static load (id: number) {
  240. return ActorModel.unscoped().findById(id)
  241. }
  242. static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) {
  243. const query = {
  244. include: [
  245. {
  246. attributes: [ 'id' ],
  247. model: AccountModel.unscoped(),
  248. required: true,
  249. include: [
  250. {
  251. attributes: [ 'id' ],
  252. model: VideoChannelModel.unscoped(),
  253. required: true,
  254. include: {
  255. attributes: [ 'id' ],
  256. model: VideoModel.unscoped(),
  257. required: true,
  258. where: {
  259. id: videoId
  260. }
  261. }
  262. }
  263. ]
  264. }
  265. ],
  266. transaction
  267. }
  268. return ActorModel.unscoped().findOne(query as any) // FIXME: typings
  269. }
  270. static isActorUrlExist (url: string) {
  271. const query = {
  272. raw: true,
  273. where: {
  274. url
  275. }
  276. }
  277. return ActorModel.unscoped().findOne(query)
  278. .then(a => !!a)
  279. }
  280. static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
  281. const query = {
  282. where: {
  283. followersUrl: {
  284. [ Sequelize.Op.in ]: followersUrls
  285. }
  286. },
  287. transaction
  288. }
  289. return ActorModel.scope(ScopeNames.FULL).findAll(query)
  290. }
  291. static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) {
  292. const query = {
  293. where: {
  294. preferredUsername,
  295. serverId: null
  296. },
  297. transaction
  298. }
  299. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  300. }
  301. static loadByNameAndHost (preferredUsername: string, host: string) {
  302. const query = {
  303. where: {
  304. preferredUsername
  305. },
  306. include: [
  307. {
  308. model: ServerModel,
  309. required: true,
  310. where: {
  311. host
  312. }
  313. }
  314. ]
  315. }
  316. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  317. }
  318. static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
  319. const query = {
  320. where: {
  321. url
  322. },
  323. transaction,
  324. include: [
  325. {
  326. attributes: [ 'id' ],
  327. model: AccountModel.unscoped(),
  328. required: false
  329. },
  330. {
  331. attributes: [ 'id' ],
  332. model: VideoChannelModel.unscoped(),
  333. required: false
  334. }
  335. ]
  336. }
  337. return ActorModel.unscoped().findOne(query)
  338. }
  339. static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) {
  340. const query = {
  341. where: {
  342. url
  343. },
  344. transaction
  345. }
  346. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  347. }
  348. static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
  349. // FIXME: typings
  350. return (ActorModel as any).increment(column, {
  351. by,
  352. where: {
  353. id
  354. }
  355. })
  356. }
  357. toFormattedJSON () {
  358. let avatar: Avatar = null
  359. if (this.Avatar) {
  360. avatar = this.Avatar.toFormattedJSON()
  361. }
  362. return {
  363. id: this.id,
  364. url: this.url,
  365. uuid: this.uuid,
  366. name: this.preferredUsername,
  367. host: this.getHost(),
  368. hostRedundancyAllowed: this.getRedundancyAllowed(),
  369. followingCount: this.followingCount,
  370. followersCount: this.followersCount,
  371. avatar,
  372. createdAt: this.createdAt,
  373. updatedAt: this.updatedAt
  374. }
  375. }
  376. toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
  377. let activityPubType
  378. if (type === 'Account') {
  379. activityPubType = 'Person' as 'Person'
  380. } else if (type === 'Application') {
  381. activityPubType = 'Application' as 'Application'
  382. } else { // VideoChannel
  383. activityPubType = 'Group' as 'Group'
  384. }
  385. let icon = undefined
  386. if (this.avatarId) {
  387. const extension = extname(this.Avatar.filename)
  388. icon = {
  389. type: 'Image',
  390. mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
  391. url: this.getAvatarUrl()
  392. }
  393. }
  394. const json = {
  395. type: activityPubType,
  396. id: this.url,
  397. following: this.getFollowingUrl(),
  398. followers: this.getFollowersUrl(),
  399. inbox: this.inboxUrl,
  400. outbox: this.outboxUrl,
  401. preferredUsername: this.preferredUsername,
  402. url: this.url,
  403. name,
  404. endpoints: {
  405. sharedInbox: this.sharedInboxUrl
  406. },
  407. uuid: this.uuid,
  408. publicKey: {
  409. id: this.getPublicKeyUrl(),
  410. owner: this.url,
  411. publicKeyPem: this.publicKey
  412. },
  413. icon
  414. }
  415. return activityPubContextify(json)
  416. }
  417. getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
  418. const query = {
  419. attributes: [ 'sharedInboxUrl' ],
  420. include: [
  421. {
  422. attribute: [],
  423. model: ActorFollowModel.unscoped(),
  424. required: true,
  425. as: 'ActorFollowing',
  426. where: {
  427. state: 'accepted',
  428. targetActorId: this.id
  429. }
  430. }
  431. ],
  432. transaction: t
  433. }
  434. return ActorModel.findAll(query)
  435. .then(accounts => accounts.map(a => a.sharedInboxUrl))
  436. }
  437. getFollowingUrl () {
  438. return this.url + '/following'
  439. }
  440. getFollowersUrl () {
  441. return this.url + '/followers'
  442. }
  443. getPublicKeyUrl () {
  444. return this.url + '#main-key'
  445. }
  446. isOwned () {
  447. return this.serverId === null
  448. }
  449. getWebfingerUrl () {
  450. return 'acct:' + this.preferredUsername + '@' + this.getHost()
  451. }
  452. getIdentifier () {
  453. return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
  454. }
  455. getHost () {
  456. return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
  457. }
  458. getRedundancyAllowed () {
  459. return this.Server ? this.Server.redundancyAllowed : false
  460. }
  461. getAvatarUrl () {
  462. if (!this.avatarId) return undefined
  463. return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
  464. }
  465. isOutdated () {
  466. if (this.isOwned()) return false
  467. const now = Date.now()
  468. const createdAtTime = this.createdAt.getTime()
  469. const updatedAtTime = this.updatedAt.getTime()
  470. return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
  471. (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
  472. }
  473. }