activity-pub-utils.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { ContextType } from '@peertube/peertube-models'
  2. import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
  3. import { buildDigest } from './peertube-crypto.js'
  4. import type { signJsonLDObject } from './peertube-jsonld.js'
  5. import { doJSONRequest } from './requests.js'
  6. import { isArray } from './custom-validators/misc.js'
  7. export type ContextFilter = <T> (arg: T) => Promise<T>
  8. export function buildGlobalHTTPHeaders (
  9. body: any,
  10. digestBuilder: typeof buildDigest
  11. ) {
  12. return {
  13. 'digest': digestBuilder(body),
  14. 'content-type': 'application/activity+json',
  15. 'accept': ACTIVITY_PUB.ACCEPT_HEADER
  16. }
  17. }
  18. export async function activityPubContextify <T> (data: T, type: ContextType, contextFilter: ContextFilter) {
  19. return { ...await getContextData(type, contextFilter), ...data }
  20. }
  21. export async function signAndContextify <T> (options: {
  22. byActor: { url: string, privateKey: string }
  23. data: T
  24. contextType: ContextType | null
  25. contextFilter: ContextFilter
  26. signerFunction: typeof signJsonLDObject<T>
  27. }) {
  28. const { byActor, data, contextType, contextFilter, signerFunction } = options
  29. const activity = contextType
  30. ? await activityPubContextify(data, contextType, contextFilter)
  31. : data
  32. return signerFunction({ byActor, data: activity })
  33. }
  34. export async function getApplicationActorOfHost (host: string) {
  35. const url = REMOTE_SCHEME.HTTP + '://' + host + '/.well-known/nodeinfo'
  36. const { body } = await doJSONRequest<{ links: { rel: string, href: string }[] }>(url)
  37. if (!isArray(body.links)) return undefined
  38. const found = body.links.find(l => l.rel === 'https://www.w3.org/ns/activitystreams#Application')
  39. return found?.href || undefined
  40. }
  41. // ---------------------------------------------------------------------------
  42. // Private
  43. // ---------------------------------------------------------------------------
  44. type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) }
  45. const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
  46. Video: buildContext({
  47. Hashtag: 'as:Hashtag',
  48. uuid: 'sc:identifier',
  49. category: 'sc:category',
  50. licence: 'sc:license',
  51. subtitleLanguage: 'sc:subtitleLanguage',
  52. sensitive: 'as:sensitive',
  53. language: 'sc:inLanguage',
  54. identifier: 'sc:identifier',
  55. isLiveBroadcast: 'sc:isLiveBroadcast',
  56. liveSaveReplay: {
  57. '@type': 'sc:Boolean',
  58. '@id': 'pt:liveSaveReplay'
  59. },
  60. permanentLive: {
  61. '@type': 'sc:Boolean',
  62. '@id': 'pt:permanentLive'
  63. },
  64. latencyMode: {
  65. '@type': 'sc:Number',
  66. '@id': 'pt:latencyMode'
  67. },
  68. Infohash: 'pt:Infohash',
  69. tileWidth: {
  70. '@type': 'sc:Number',
  71. '@id': 'pt:tileWidth'
  72. },
  73. tileHeight: {
  74. '@type': 'sc:Number',
  75. '@id': 'pt:tileHeight'
  76. },
  77. tileDuration: {
  78. '@type': 'sc:Number',
  79. '@id': 'pt:tileDuration'
  80. },
  81. originallyPublishedAt: 'sc:datePublished',
  82. uploadDate: 'sc:uploadDate',
  83. hasParts: 'sc:hasParts',
  84. views: {
  85. '@type': 'sc:Number',
  86. '@id': 'pt:views'
  87. },
  88. state: {
  89. '@type': 'sc:Number',
  90. '@id': 'pt:state'
  91. },
  92. size: {
  93. '@type': 'sc:Number',
  94. '@id': 'pt:size'
  95. },
  96. fps: {
  97. '@type': 'sc:Number',
  98. '@id': 'pt:fps'
  99. },
  100. commentsEnabled: {
  101. '@type': 'sc:Boolean',
  102. '@id': 'pt:commentsEnabled'
  103. },
  104. downloadEnabled: {
  105. '@type': 'sc:Boolean',
  106. '@id': 'pt:downloadEnabled'
  107. },
  108. waitTranscoding: {
  109. '@type': 'sc:Boolean',
  110. '@id': 'pt:waitTranscoding'
  111. },
  112. support: {
  113. '@type': 'sc:Text',
  114. '@id': 'pt:support'
  115. },
  116. likes: {
  117. '@id': 'as:likes',
  118. '@type': '@id'
  119. },
  120. dislikes: {
  121. '@id': 'as:dislikes',
  122. '@type': '@id'
  123. },
  124. shares: {
  125. '@id': 'as:shares',
  126. '@type': '@id'
  127. },
  128. comments: {
  129. '@id': 'as:comments',
  130. '@type': '@id'
  131. }
  132. }),
  133. Playlist: buildContext({
  134. Playlist: 'pt:Playlist',
  135. PlaylistElement: 'pt:PlaylistElement',
  136. position: {
  137. '@type': 'sc:Number',
  138. '@id': 'pt:position'
  139. },
  140. startTimestamp: {
  141. '@type': 'sc:Number',
  142. '@id': 'pt:startTimestamp'
  143. },
  144. stopTimestamp: {
  145. '@type': 'sc:Number',
  146. '@id': 'pt:stopTimestamp'
  147. },
  148. uuid: 'sc:identifier'
  149. }),
  150. CacheFile: buildContext({
  151. expires: 'sc:expires',
  152. CacheFile: 'pt:CacheFile'
  153. }),
  154. Flag: buildContext({
  155. Hashtag: 'as:Hashtag'
  156. }),
  157. Actor: buildContext({
  158. playlists: {
  159. '@id': 'pt:playlists',
  160. '@type': '@id'
  161. },
  162. support: {
  163. '@type': 'sc:Text',
  164. '@id': 'pt:support'
  165. },
  166. // TODO: remove in a few versions, introduced in 4.2
  167. icons: 'as:icon'
  168. }),
  169. WatchAction: buildContext({
  170. WatchAction: 'sc:WatchAction',
  171. startTimestamp: {
  172. '@type': 'sc:Number',
  173. '@id': 'pt:startTimestamp'
  174. },
  175. stopTimestamp: {
  176. '@type': 'sc:Number',
  177. '@id': 'pt:stopTimestamp'
  178. },
  179. watchSection: {
  180. '@type': 'sc:Number',
  181. '@id': 'pt:stopTimestamp'
  182. },
  183. uuid: 'sc:identifier'
  184. }),
  185. View: buildContext({
  186. WatchAction: 'sc:WatchAction',
  187. InteractionCounter: 'sc:InteractionCounter',
  188. interactionType: 'sc:interactionType',
  189. userInteractionCount: 'sc:userInteractionCount'
  190. }),
  191. Collection: buildContext(),
  192. Follow: buildContext(),
  193. Reject: buildContext(),
  194. Accept: buildContext(),
  195. Announce: buildContext(),
  196. Comment: buildContext(),
  197. Delete: buildContext(),
  198. Rate: buildContext(),
  199. Chapters: buildContext({
  200. name: 'sc:name',
  201. hasPart: 'sc:hasPart',
  202. endOffset: 'sc:endOffset',
  203. startOffset: 'sc:startOffset'
  204. })
  205. }
  206. async function getContextData (type: ContextType, contextFilter: ContextFilter) {
  207. const contextData = contextFilter
  208. ? await contextFilter(contextStore[type])
  209. : contextStore[type]
  210. return { '@context': contextData }
  211. }
  212. function buildContext (contextValue?: ContextValue) {
  213. const baseContext = [
  214. 'https://www.w3.org/ns/activitystreams',
  215. 'https://w3id.org/security/v1',
  216. {
  217. RsaSignature2017: 'https://w3id.org/security#RsaSignature2017'
  218. }
  219. ]
  220. if (!contextValue) return baseContext
  221. return [
  222. ...baseContext,
  223. {
  224. pt: 'https://joinpeertube.org/ns#',
  225. sc: 'http://schema.org/',
  226. ...contextValue
  227. }
  228. ]
  229. }