ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

activity-pub-utils.ts 8.2 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import { ContextType } from '@peertube/peertube-models'
  2. import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
  3. import { isArray } from './custom-validators/misc.js'
  4. import { buildDigest } from './peertube-crypto.js'
  5. import type { signJsonLDObject } from './peertube-jsonld.js'
  6. import { doJSONRequest } from './requests.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. export function getAPPublicValue (): 'https://www.w3.org/ns/activitystreams#Public' {
  42. return 'https://www.w3.org/ns/activitystreams#Public'
  43. }
  44. export function hasAPPublic (toOrCC: string[]) {
  45. if (!isArray(toOrCC)) return false
  46. const publicValue = getAPPublicValue()
  47. return toOrCC.some(f => f === 'as:Public' || publicValue)
  48. }
  49. // ---------------------------------------------------------------------------
  50. // Private
  51. // ---------------------------------------------------------------------------
  52. type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) }
  53. const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
  54. Video: buildContext({
  55. Hashtag: 'as:Hashtag',
  56. category: 'sc:category',
  57. licence: 'sc:license',
  58. subtitleLanguage: 'sc:subtitleLanguage',
  59. automaticallyGenerated: 'pt:automaticallyGenerated',
  60. sensitive: 'as:sensitive',
  61. language: 'sc:inLanguage',
  62. identifier: 'sc:identifier',
  63. isLiveBroadcast: 'sc:isLiveBroadcast',
  64. liveSaveReplay: {
  65. '@type': 'sc:Boolean',
  66. '@id': 'pt:liveSaveReplay'
  67. },
  68. permanentLive: {
  69. '@type': 'sc:Boolean',
  70. '@id': 'pt:permanentLive'
  71. },
  72. latencyMode: {
  73. '@type': 'sc:Number',
  74. '@id': 'pt:latencyMode'
  75. },
  76. Infohash: 'pt:Infohash',
  77. tileWidth: {
  78. '@type': 'sc:Number',
  79. '@id': 'pt:tileWidth'
  80. },
  81. tileHeight: {
  82. '@type': 'sc:Number',
  83. '@id': 'pt:tileHeight'
  84. },
  85. tileDuration: {
  86. '@type': 'sc:Number',
  87. '@id': 'pt:tileDuration'
  88. },
  89. aspectRatio: {
  90. '@type': 'sc:Float',
  91. '@id': 'pt:aspectRatio'
  92. },
  93. uuid: {
  94. '@type': 'sc:identifier',
  95. '@id': 'pt:uuid'
  96. },
  97. originallyPublishedAt: 'sc:datePublished',
  98. uploadDate: 'sc:uploadDate',
  99. hasParts: 'sc:hasParts',
  100. views: {
  101. '@type': 'sc:Number',
  102. '@id': 'pt:views'
  103. },
  104. state: {
  105. '@type': 'sc:Number',
  106. '@id': 'pt:state'
  107. },
  108. size: {
  109. '@type': 'sc:Number',
  110. '@id': 'pt:size'
  111. },
  112. fps: {
  113. '@type': 'sc:Number',
  114. '@id': 'pt:fps'
  115. },
  116. // Keep for federation compatibility
  117. commentsEnabled: {
  118. '@type': 'sc:Boolean',
  119. '@id': 'pt:commentsEnabled'
  120. },
  121. canReply: 'pt:canReply',
  122. commentsPolicy: {
  123. '@type': 'sc:Number',
  124. '@id': 'pt:commentsPolicy'
  125. },
  126. downloadEnabled: {
  127. '@type': 'sc:Boolean',
  128. '@id': 'pt:downloadEnabled'
  129. },
  130. waitTranscoding: {
  131. '@type': 'sc:Boolean',
  132. '@id': 'pt:waitTranscoding'
  133. },
  134. support: {
  135. '@type': 'sc:Text',
  136. '@id': 'pt:support'
  137. },
  138. likes: {
  139. '@id': 'as:likes',
  140. '@type': '@id'
  141. },
  142. dislikes: {
  143. '@id': 'as:dislikes',
  144. '@type': '@id'
  145. },
  146. shares: {
  147. '@id': 'as:shares',
  148. '@type': '@id'
  149. },
  150. comments: {
  151. '@id': 'as:comments',
  152. '@type': '@id'
  153. }
  154. }),
  155. Playlist: buildContext({
  156. Playlist: 'pt:Playlist',
  157. PlaylistElement: 'pt:PlaylistElement',
  158. position: {
  159. '@type': 'sc:Number',
  160. '@id': 'pt:position'
  161. },
  162. startTimestamp: {
  163. '@type': 'sc:Number',
  164. '@id': 'pt:startTimestamp'
  165. },
  166. stopTimestamp: {
  167. '@type': 'sc:Number',
  168. '@id': 'pt:stopTimestamp'
  169. },
  170. uuid: {
  171. '@type': 'sc:identifier',
  172. '@id': 'pt:uuid'
  173. }
  174. }),
  175. CacheFile: buildContext({
  176. expires: 'sc:expires',
  177. CacheFile: 'pt:CacheFile',
  178. size: {
  179. '@type': 'sc:Number',
  180. '@id': 'pt:size'
  181. },
  182. fps: {
  183. '@type': 'sc:Number',
  184. '@id': 'pt:fps'
  185. }
  186. }),
  187. Flag: buildContext({
  188. Hashtag: 'as:Hashtag'
  189. }),
  190. Actor: buildContext({
  191. playlists: {
  192. '@id': 'pt:playlists',
  193. '@type': '@id'
  194. },
  195. support: {
  196. '@type': 'sc:Text',
  197. '@id': 'pt:support'
  198. },
  199. lemmy: 'https://join-lemmy.org/ns#',
  200. postingRestrictedToMods: 'lemmy:postingRestrictedToMods',
  201. // TODO: remove in a few versions, introduced in 4.2
  202. icons: 'as:icon'
  203. }),
  204. WatchAction: buildContext({
  205. WatchAction: 'sc:WatchAction',
  206. startTimestamp: {
  207. '@type': 'sc:Number',
  208. '@id': 'pt:startTimestamp'
  209. },
  210. endTimestamp: {
  211. '@type': 'sc:Number',
  212. '@id': 'pt:endTimestamp'
  213. },
  214. uuid: {
  215. '@type': 'sc:identifier',
  216. '@id': 'pt:uuid'
  217. },
  218. actionStatus: 'sc:actionStatus',
  219. watchSections: {
  220. '@type': '@id',
  221. '@id': 'pt:watchSections'
  222. },
  223. addressRegion: 'sc:addressRegion',
  224. addressCountry: 'sc:addressCountry'
  225. }),
  226. View: buildContext({
  227. WatchAction: 'sc:WatchAction',
  228. InteractionCounter: 'sc:InteractionCounter',
  229. interactionType: 'sc:interactionType',
  230. userInteractionCount: 'sc:userInteractionCount'
  231. }),
  232. Collection: buildContext(),
  233. Follow: buildContext(),
  234. Reject: buildContext(),
  235. Accept: buildContext(),
  236. Announce: buildContext(),
  237. Comment: buildContext({
  238. replyApproval: 'pt:replyApproval'
  239. }),
  240. Delete: buildContext(),
  241. Rate: buildContext(),
  242. ApproveReply: buildContext({
  243. ApproveReply: 'pt:ApproveReply'
  244. }),
  245. RejectReply: buildContext({
  246. RejectReply: 'pt:RejectReply'
  247. }),
  248. Chapters: buildContext({
  249. hasPart: 'sc:hasPart',
  250. endOffset: 'sc:endOffset',
  251. startOffset: 'sc:startOffset'
  252. })
  253. }
  254. let allContext: (string | ContextValue)[]
  255. export function getAllContext () {
  256. if (allContext) return allContext
  257. const processed = new Set<string>()
  258. allContext = []
  259. let staticContext: ContextValue = {}
  260. for (const v of Object.values(contextStore)) {
  261. for (const item of v) {
  262. if (typeof item === 'string') {
  263. if (!processed.has(item)) {
  264. allContext.push(item)
  265. }
  266. processed.add(item)
  267. } else {
  268. for (const subKey of Object.keys(item)) {
  269. if (!processed.has(subKey)) {
  270. staticContext = { ...staticContext, [subKey]: item[subKey] }
  271. }
  272. processed.add(subKey)
  273. }
  274. }
  275. }
  276. }
  277. allContext = [ ...allContext, staticContext ]
  278. return allContext
  279. }
  280. async function getContextData (type: ContextType, contextFilter: ContextFilter) {
  281. const contextData = contextFilter
  282. ? await contextFilter(contextStore[type])
  283. : contextStore[type]
  284. return { '@context': contextData }
  285. }
  286. function buildContext (contextValue?: ContextValue) {
  287. const baseContext = [
  288. 'https://www.w3.org/ns/activitystreams',
  289. 'https://w3id.org/security/v1',
  290. {
  291. RsaSignature2017: 'https://w3id.org/security#RsaSignature2017'
  292. }
  293. ]
  294. if (!contextValue) return baseContext
  295. return [
  296. ...baseContext,
  297. {
  298. pt: 'https://joinpeertube.org/ns#',
  299. sc: 'http://schema.org/',
  300. ...contextValue
  301. }
  302. ]
  303. }