logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

streaming_operation.ex (13177B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ApiSpec.StreamingOperation do
  5. alias OpenApiSpex.Operation
  6. alias OpenApiSpex.Response
  7. alias OpenApiSpex.Schema
  8. alias Pleroma.Web.ApiSpec.NotificationOperation
  9. alias Pleroma.Web.ApiSpec.Schemas.Chat
  10. alias Pleroma.Web.ApiSpec.Schemas.Conversation
  11. alias Pleroma.Web.ApiSpec.Schemas.FlakeID
  12. alias Pleroma.Web.ApiSpec.Schemas.Status
  13. require Pleroma.Constants
  14. @spec open_api_operation(atom) :: Operation.t()
  15. def open_api_operation(action) do
  16. operation = String.to_existing_atom("#{action}_operation")
  17. apply(__MODULE__, operation, [])
  18. end
  19. @spec streaming_operation() :: Operation.t()
  20. def streaming_operation do
  21. %Operation{
  22. tags: ["Timelines"],
  23. summary: "Establish streaming connection",
  24. description: """
  25. Receive statuses in real-time via WebSocket.
  26. You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
  27. the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
  28. your client's compatibility with Mastodon).
  29. You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users,
  30. you must specify the access token at the time of the connection (i.e. via query string or header).
  31. Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
  32. The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
  33. describes what events server will send through WebSocket.
  34. """,
  35. security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
  36. operationId: "WebsocketHandler.streaming",
  37. parameters:
  38. [
  39. Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
  40. required: true
  41. ),
  42. Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
  43. required: true
  44. ),
  45. Operation.parameter(
  46. :"sec-websocket-key",
  47. :header,
  48. %Schema{type: :string},
  49. "sec-websocket-key header",
  50. required: true
  51. ),
  52. Operation.parameter(
  53. :"sec-websocket-version",
  54. :header,
  55. %Schema{type: :string},
  56. "sec-websocket-version header",
  57. required: true
  58. )
  59. ] ++ stream_params() ++ access_token_params(),
  60. requestBody: request_body("Client-sent events", client_sent_events()),
  61. responses: %{
  62. 101 => switching_protocols_response(),
  63. 200 =>
  64. Operation.response(
  65. "Server-sent events",
  66. "application/json",
  67. server_sent_events()
  68. )
  69. }
  70. }
  71. end
  72. defp stream_params do
  73. stream_specifier()
  74. |> Enum.map(fn {name, schema} ->
  75. Operation.parameter(name, :query, schema, get_schema(schema).description)
  76. end)
  77. end
  78. defp access_token_params do
  79. [
  80. Operation.parameter(:access_token, :query, token(), token().description),
  81. Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
  82. ]
  83. end
  84. defp switching_protocols_response do
  85. %Response{
  86. description: "Switching protocols",
  87. headers: %{
  88. "connection" => %OpenApiSpex.Header{required: true},
  89. "upgrade" => %OpenApiSpex.Header{required: true},
  90. "sec-websocket-accept" => %OpenApiSpex.Header{required: true}
  91. }
  92. }
  93. end
  94. defp server_sent_events do
  95. %Schema{
  96. oneOf: [
  97. update_event(),
  98. status_update_event(),
  99. notification_event(),
  100. chat_update_event(),
  101. follow_relationships_update_event(),
  102. conversation_event(),
  103. delete_event(),
  104. pleroma_respond_event()
  105. ]
  106. }
  107. end
  108. defp stream do
  109. %Schema{
  110. type: :array,
  111. title: "Stream",
  112. description: """
  113. The stream identifier.
  114. The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
  115. Currently, for the following stream types, there is a second element in the array:
  116. - `list`: The second element is the id of the list, as a string.
  117. - `hashtag`: The second element is the name of the hashtag.
  118. - `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
  119. """,
  120. maxItems: 2,
  121. minItems: 1,
  122. items: %Schema{type: :string},
  123. example: ["hashtag", "mew"]
  124. }
  125. end
  126. defp get_schema(%Schema{} = schema), do: schema
  127. defp get_schema(schema), do: schema.schema
  128. defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
  129. payload_type = Keyword.get(opts, :payload_type, :json)
  130. has_stream = Keyword.get(opts, :has_stream, true)
  131. stream_properties =
  132. if has_stream do
  133. %{stream: stream()}
  134. else
  135. %{}
  136. end
  137. stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
  138. stream_required = if has_stream, do: [:stream], else: []
  139. payload_schema =
  140. if payload_type == :json do
  141. %Schema{
  142. title: "Event payload",
  143. description: "JSON-encoded string of #{get_schema(payload).title}",
  144. allOf: [payload]
  145. }
  146. else
  147. payload
  148. end
  149. payload_example =
  150. if payload_type == :json do
  151. get_schema(payload).example |> Jason.encode!()
  152. else
  153. get_schema(payload).example
  154. end
  155. %Schema{
  156. type: :object,
  157. title: name,
  158. description: description,
  159. required: [:event, :payload] ++ stream_required,
  160. properties:
  161. %{
  162. event: %Schema{
  163. title: "Event type",
  164. description: "Type of the event.",
  165. type: :string,
  166. required: true,
  167. enum: [type]
  168. },
  169. payload: payload_schema
  170. }
  171. |> Map.merge(stream_properties),
  172. example:
  173. %{
  174. "event" => type,
  175. "payload" => payload_example
  176. }
  177. |> Map.merge(stream_example)
  178. }
  179. end
  180. defp update_event do
  181. server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
  182. end
  183. defp status_update_event do
  184. server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
  185. end
  186. defp notification_event do
  187. server_sent_event_helper(
  188. "Notification",
  189. "A new notification.",
  190. "notification",
  191. NotificationOperation.notification()
  192. )
  193. end
  194. defp follow_relationships_update_event do
  195. server_sent_event_helper(
  196. "Follow relationships update",
  197. "An update to follow relationships.",
  198. "pleroma:follow_relationships_update",
  199. %Schema{
  200. type: :object,
  201. title: "Follow relationships update",
  202. required: [:state, :follower, :following],
  203. properties: %{
  204. state: %Schema{
  205. type: :string,
  206. description: "Follow state of the relationship.",
  207. enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
  208. },
  209. follower: %Schema{
  210. type: :object,
  211. description: "Information about the follower.",
  212. required: [:id, :follower_count, :following_count],
  213. properties: %{
  214. id: FlakeID,
  215. follower_count: %Schema{type: :integer},
  216. following_count: %Schema{type: :integer}
  217. }
  218. },
  219. following: %Schema{
  220. type: :object,
  221. description: "Information about the following person.",
  222. required: [:id, :follower_count, :following_count],
  223. properties: %{
  224. id: FlakeID,
  225. follower_count: %Schema{type: :integer},
  226. following_count: %Schema{type: :integer}
  227. }
  228. }
  229. },
  230. example: %{
  231. "state" => "follow_pending",
  232. "follower" => %{
  233. "id" => "someUser1",
  234. "follower_count" => 1,
  235. "following_count" => 1
  236. },
  237. "following" => %{
  238. "id" => "someUser2",
  239. "follower_count" => 1,
  240. "following_count" => 1
  241. }
  242. }
  243. }
  244. )
  245. end
  246. defp chat_update_event do
  247. server_sent_event_helper(
  248. "Chat update",
  249. "A new chat message.",
  250. "pleroma:chat_update",
  251. Chat
  252. )
  253. end
  254. defp conversation_event do
  255. server_sent_event_helper(
  256. "Conversation update",
  257. "An update about a conversation",
  258. "conversation",
  259. Conversation
  260. )
  261. end
  262. defp delete_event do
  263. server_sent_event_helper(
  264. "Delete",
  265. "A status that was just deleted.",
  266. "delete",
  267. %Schema{
  268. type: :string,
  269. title: "Status id",
  270. description: "Id of the deleted status",
  271. allOf: [FlakeID],
  272. example: "some-opaque-id"
  273. },
  274. payload_type: :string,
  275. has_stream: false
  276. )
  277. end
  278. defp pleroma_respond_event do
  279. server_sent_event_helper(
  280. "Server response",
  281. "A response to a client-sent event.",
  282. "pleroma:respond",
  283. %Schema{
  284. type: :object,
  285. title: "Results",
  286. required: [:result, :type],
  287. properties: %{
  288. result: %Schema{
  289. type: :string,
  290. title: "Result of the request",
  291. enum: ["success", "error", "ignored"]
  292. },
  293. error: %Schema{
  294. type: :string,
  295. title: "Error code",
  296. description: "An error identifier. Only appears if `result` is `error`."
  297. },
  298. type: %Schema{
  299. type: :string,
  300. description: "Type of the request."
  301. }
  302. },
  303. example: %{"result" => "success", "type" => "pleroma:authenticate"}
  304. },
  305. has_stream: false
  306. )
  307. end
  308. defp client_sent_events do
  309. %Schema{
  310. oneOf: [
  311. subscribe_event(),
  312. unsubscribe_event(),
  313. authenticate_event()
  314. ]
  315. }
  316. end
  317. defp request_body(description, schema, opts \\ []) do
  318. %OpenApiSpex.RequestBody{
  319. description: description,
  320. content: %{
  321. "application/json" => %OpenApiSpex.MediaType{
  322. schema: schema,
  323. example: opts[:example],
  324. examples: opts[:examples]
  325. }
  326. }
  327. }
  328. end
  329. defp client_sent_event_helper(name, description, type, properties, opts) do
  330. required = opts[:required] || []
  331. %Schema{
  332. type: :object,
  333. title: name,
  334. required: [:type] ++ required,
  335. description: description,
  336. properties:
  337. %{
  338. type: %Schema{type: :string, enum: [type], description: "Type of the event."}
  339. }
  340. |> Map.merge(properties),
  341. example: opts[:example]
  342. }
  343. end
  344. defp subscribe_event do
  345. client_sent_event_helper(
  346. "Subscribe",
  347. "Subscribe to a stream.",
  348. "subscribe",
  349. stream_specifier(),
  350. required: [:stream],
  351. example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
  352. )
  353. end
  354. defp unsubscribe_event do
  355. client_sent_event_helper(
  356. "Unsubscribe",
  357. "Unsubscribe from a stream.",
  358. "unsubscribe",
  359. stream_specifier(),
  360. required: [:stream],
  361. example: %{
  362. "type" => "unsubscribe",
  363. "stream" => "public:remote:media",
  364. "instance" => "example.org"
  365. }
  366. )
  367. end
  368. defp authenticate_event do
  369. client_sent_event_helper(
  370. "Authenticate",
  371. "Authenticate via an access token.",
  372. "pleroma:authenticate",
  373. %{
  374. token: token()
  375. },
  376. required: [:token]
  377. )
  378. end
  379. defp token do
  380. %Schema{
  381. type: :string,
  382. description: "An OAuth access token with corresponding permissions.",
  383. example: "some token"
  384. }
  385. end
  386. defp stream_specifier do
  387. %{
  388. stream: %Schema{
  389. type: :string,
  390. description: "The name of the stream.",
  391. enum:
  392. Pleroma.Constants.public_streams() ++
  393. [
  394. "public:remote",
  395. "public:remote:media",
  396. "user",
  397. "user:pleroma_chat",
  398. "user:notification",
  399. "direct",
  400. "list",
  401. "hashtag"
  402. ]
  403. },
  404. list: %Schema{
  405. type: :string,
  406. title: "List id",
  407. description: "The id of the list. Required when `stream` is `list`.",
  408. example: "some-id"
  409. },
  410. tag: %Schema{
  411. type: :string,
  412. title: "Hashtag name",
  413. description: "The name of the hashtag. Required when `stream` is `hashtag`.",
  414. example: "mew"
  415. },
  416. instance: %Schema{
  417. type: :string,
  418. title: "Domain name",
  419. description:
  420. "Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
  421. example: "example.org"
  422. }
  423. }
  424. end
  425. end