actor.ts 12 KB

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