1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.ApiSpec.StreamingOperation do
6 alias OpenApiSpex.Operation
7 alias OpenApiSpex.Response
8 alias OpenApiSpex.Schema
9 alias Pleroma.Web.ApiSpec.NotificationOperation
10 alias Pleroma.Web.ApiSpec.Schemas.Chat
11 alias Pleroma.Web.ApiSpec.Schemas.Conversation
12 alias Pleroma.Web.ApiSpec.Schemas.FlakeID
13 alias Pleroma.Web.ApiSpec.Schemas.Status
15 require Pleroma.Constants
17 @spec open_api_operation(atom) :: Operation.t()
18 def open_api_operation(action) do
19 operation = String.to_existing_atom("#{action}_operation")
20 apply(__MODULE__, operation, [])
23 @spec streaming_operation() :: Operation.t()
24 def streaming_operation do
27 summary: "Establish streaming connection",
29 Receive statuses in real-time via WebSocket.
31 You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
32 the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
33 your client's compatibility with Mastodon).
35 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,
36 you must specify the access token at the time of the connection (i.e. via query string or header).
38 Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
40 The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
41 describes what events server will send through WebSocket.
43 security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
44 operationId: "WebsocketHandler.streaming",
47 Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
50 Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
56 %Schema{type: :string},
57 "sec-websocket-key header",
61 :"sec-websocket-version",
63 %Schema{type: :string},
64 "sec-websocket-version header",
67 ] ++ stream_params() ++ access_token_params(),
68 requestBody: request_body("Client-sent events", client_sent_events()),
70 101 => switching_protocols_response(),
83 |> Enum.map(fn {name, schema} ->
84 Operation.parameter(name, :query, schema, get_schema(schema).description)
88 defp access_token_params do
90 Operation.parameter(:access_token, :query, token(), token().description),
91 Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
95 defp switching_protocols_response do
97 description: "Switching protocols",
99 "connection" => %OpenApiSpex.Header{required: true},
100 "upgrade" => %OpenApiSpex.Header{required: true},
101 "sec-websocket-accept" => %OpenApiSpex.Header{required: true}
106 defp server_sent_events do
110 status_update_event(),
111 notification_event(),
113 follow_relationships_update_event(),
114 conversation_event(),
116 pleroma_respond_event()
126 The stream identifier.
127 The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
128 Currently, for the following stream types, there is a second element in the array:
130 - `list`: The second element is the id of the list, as a string.
131 - `hashtag`: The second element is the name of the hashtag.
132 - `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
136 items: %Schema{type: :string},
137 example: ["hashtag", "mew"]
141 defp get_schema(%Schema{} = schema), do: schema
142 defp get_schema(schema), do: schema.schema
144 defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
145 payload_type = Keyword.get(opts, :payload_type, :json)
146 has_stream = Keyword.get(opts, :has_stream, true)
155 stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
157 stream_required = if has_stream, do: [:stream], else: []
160 if payload_type == :json do
162 title: "Event payload",
163 description: "JSON-encoded string of #{get_schema(payload).title}",
171 if payload_type == :json do
172 get_schema(payload).example |> Jason.encode!()
174 get_schema(payload).example
180 description: description,
181 required: [:event, :payload] ++ stream_required,
186 description: "Type of the event.",
191 payload: payload_schema
193 |> Map.merge(stream_properties),
197 "payload" => payload_example
199 |> Map.merge(stream_example)
204 server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
207 defp status_update_event do
208 server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
211 defp notification_event do
212 server_sent_event_helper(
214 "A new notification.",
216 NotificationOperation.notification()
220 defp follow_relationships_update_event do
221 server_sent_event_helper(
222 "Follow relationships update",
223 "An update to follow relationships.",
224 "pleroma:follow_relationships_update",
227 title: "Follow relationships update",
228 required: [:state, :follower, :following],
232 description: "Follow state of the relationship.",
233 enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
237 description: "Information about the follower.",
238 required: [:id, :follower_count, :following_count],
241 follower_count: %Schema{type: :integer},
242 following_count: %Schema{type: :integer}
247 description: "Information about the following person.",
248 required: [:id, :follower_count, :following_count],
251 follower_count: %Schema{type: :integer},
252 following_count: %Schema{type: :integer}
257 "state" => "follow_pending",
260 "follower_count" => 1,
261 "following_count" => 1
265 "follower_count" => 1,
266 "following_count" => 1
273 defp chat_update_event do
274 server_sent_event_helper(
276 "A new chat message.",
277 "pleroma:chat_update",
282 defp conversation_event do
283 server_sent_event_helper(
284 "Conversation update",
285 "An update about a conversation",
292 server_sent_event_helper(
294 "A status that was just deleted.",
299 description: "Id of the deleted status",
301 example: "some-opaque-id"
303 payload_type: :string,
308 defp pleroma_respond_event do
309 server_sent_event_helper(
311 "A response to a client-sent event.",
316 required: [:result, :type],
320 title: "Result of the request",
321 enum: ["success", "error", "ignored"]
326 description: "An error identifier. Only appears if `result` is `error`."
330 description: "Type of the request."
333 example: %{"result" => "success", "type" => "pleroma:authenticate"}
339 defp client_sent_events do
349 defp request_body(description, schema, opts \\ []) do
350 %OpenApiSpex.RequestBody{
351 description: description,
353 "application/json" => %OpenApiSpex.MediaType{
355 example: opts[:example],
356 examples: opts[:examples]
362 defp client_sent_event_helper(name, description, type, properties, opts) do
363 required = opts[:required] || []
368 required: [:type] ++ required,
369 description: description,
372 type: %Schema{type: :string, enum: [type], description: "Type of the event."}
374 |> Map.merge(properties),
375 example: opts[:example]
379 defp subscribe_event do
380 client_sent_event_helper(
382 "Subscribe to a stream.",
386 example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
390 defp unsubscribe_event do
391 client_sent_event_helper(
393 "Unsubscribe from a stream.",
398 "type" => "unsubscribe",
399 "stream" => "public:remote:media",
400 "instance" => "example.org"
405 defp authenticate_event do
406 client_sent_event_helper(
408 "Authenticate via an access token.",
409 "pleroma:authenticate",
420 description: "An OAuth access token with corresponding permissions.",
421 example: "some token"
425 defp stream_specifier do
429 description: "The name of the stream.",
431 Pleroma.Constants.public_streams() ++
434 "public:remote:media",
446 description: "The id of the list. Required when `stream` is `list`.",
451 title: "Hashtag name",
452 description: "The name of the hashtag. Required when `stream` is `hashtag`.",
457 title: "Domain name",
459 "Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
460 example: "example.org"