activitypub.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import * as Bluebird from 'bluebird'
  2. import validator from 'validator'
  3. import { ResultList } from '../../shared/models'
  4. import { Activity } from '../../shared/models/activitypub'
  5. import { ACTIVITY_PUB } from '../initializers/constants'
  6. import { signJsonLDObject } from './peertube-crypto'
  7. import { pageToStartAndCount } from './core-utils'
  8. import { parse } from 'url'
  9. import { MActor } from '../typings/models'
  10. function activityPubContextify <T> (data: T) {
  11. return Object.assign(data, {
  12. '@context': [
  13. 'https://www.w3.org/ns/activitystreams',
  14. 'https://w3id.org/security/v1',
  15. {
  16. RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
  17. pt: 'https://joinpeertube.org/ns#',
  18. sc: 'http://schema.org#',
  19. Hashtag: 'as:Hashtag',
  20. uuid: 'sc:identifier',
  21. category: 'sc:category',
  22. licence: 'sc:license',
  23. subtitleLanguage: 'sc:subtitleLanguage',
  24. sensitive: 'as:sensitive',
  25. language: 'sc:inLanguage',
  26. expires: 'sc:expires',
  27. CacheFile: 'pt:CacheFile',
  28. Infohash: 'pt:Infohash',
  29. originallyPublishedAt: 'sc:datePublished',
  30. views: {
  31. '@type': 'sc:Number',
  32. '@id': 'pt:views'
  33. },
  34. state: {
  35. '@type': 'sc:Number',
  36. '@id': 'pt:state'
  37. },
  38. size: {
  39. '@type': 'sc:Number',
  40. '@id': 'pt:size'
  41. },
  42. fps: {
  43. '@type': 'sc:Number',
  44. '@id': 'pt:fps'
  45. },
  46. startTimestamp: {
  47. '@type': 'sc:Number',
  48. '@id': 'pt:startTimestamp'
  49. },
  50. stopTimestamp: {
  51. '@type': 'sc:Number',
  52. '@id': 'pt:stopTimestamp'
  53. },
  54. position: {
  55. '@type': 'sc:Number',
  56. '@id': 'pt:position'
  57. },
  58. commentsEnabled: {
  59. '@type': 'sc:Boolean',
  60. '@id': 'pt:commentsEnabled'
  61. },
  62. downloadEnabled: {
  63. '@type': 'sc:Boolean',
  64. '@id': 'pt:downloadEnabled'
  65. },
  66. waitTranscoding: {
  67. '@type': 'sc:Boolean',
  68. '@id': 'pt:waitTranscoding'
  69. },
  70. support: {
  71. '@type': 'sc:Text',
  72. '@id': 'pt:support'
  73. }
  74. },
  75. {
  76. likes: {
  77. '@id': 'as:likes',
  78. '@type': '@id'
  79. },
  80. dislikes: {
  81. '@id': 'as:dislikes',
  82. '@type': '@id'
  83. },
  84. playlists: {
  85. '@id': 'pt:playlists',
  86. '@type': '@id'
  87. },
  88. shares: {
  89. '@id': 'as:shares',
  90. '@type': '@id'
  91. },
  92. comments: {
  93. '@id': 'as:comments',
  94. '@type': '@id'
  95. }
  96. }
  97. ]
  98. })
  99. }
  100. type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
  101. async function activityPubCollectionPagination (
  102. baseUrl: string,
  103. handler: ActivityPubCollectionPaginationHandler,
  104. page?: any,
  105. size = ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE
  106. ) {
  107. if (!page || !validator.isInt(page)) {
  108. // We just display the first page URL, we only need the total items
  109. const result = await handler(0, 1)
  110. return {
  111. id: baseUrl,
  112. type: 'OrderedCollectionPage',
  113. totalItems: result.total,
  114. first: baseUrl + '?page=1'
  115. }
  116. }
  117. const { start, count } = pageToStartAndCount(page, size)
  118. const result = await handler(start, count)
  119. let next: string | undefined
  120. let prev: string | undefined
  121. // Assert page is a number
  122. page = parseInt(page, 10)
  123. // There are more results
  124. if (result.total > page * size) {
  125. next = baseUrl + '?page=' + (page + 1)
  126. }
  127. if (page > 1) {
  128. prev = baseUrl + '?page=' + (page - 1)
  129. }
  130. return {
  131. id: baseUrl + '?page=' + page,
  132. type: 'OrderedCollectionPage',
  133. prev,
  134. next,
  135. partOf: baseUrl,
  136. orderedItems: result.data,
  137. totalItems: result.total
  138. }
  139. }
  140. function buildSignedActivity (byActor: MActor, data: Object) {
  141. const activity = activityPubContextify(data)
  142. return signJsonLDObject(byActor, activity) as Promise<Activity>
  143. }
  144. function getAPId (activity: string | { id: string }) {
  145. if (typeof activity === 'string') return activity
  146. return activity.id
  147. }
  148. function checkUrlsSameHost (url1: string, url2: string) {
  149. const idHost = parse(url1).host
  150. const actorHost = parse(url2).host
  151. return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
  152. }
  153. // ---------------------------------------------------------------------------
  154. export {
  155. checkUrlsSameHost,
  156. getAPId,
  157. activityPubContextify,
  158. activityPubCollectionPagination,
  159. buildSignedActivity
  160. }