diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pleroma/application.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/upload.ex | 39 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/endpoint.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 63 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_socket.ex | 34 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/views/account_view.ex | 6 | ||||
-rw-r--r-- | lib/pleroma/web/mastodon_api/views/status_view.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/oauth/oauth_controller.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/web/streamer.ex | 76 | ||||
-rw-r--r-- | lib/transports.ex | 77 |
12 files changed, 300 insertions, 9 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 1f0a05568..5422cbc28 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -19,7 +19,8 @@ defmodule Pleroma.Application do ttl_interval: 1000, limit: 2500 ]]), - worker(Pleroma.Web.Federator, []) + worker(Pleroma.Web.Federator, []), + worker(Pleroma.Web.Streamer, []) ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 2717377a3..3567c6c88 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -8,11 +8,18 @@ defmodule Pleroma.Upload do result_file = Path.join(upload_folder, file.filename) File.cp!(file.path, result_file) + # fix content type on some image uploads + content_type = if file.content_type == "application/octet-stream" do + get_content_type(file.path) + else + file.content_type + end + %{ "type" => "Image", "url" => [%{ "type" => "Link", - "mediaType" => file.content_type, + "mediaType" => content_type, "href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename))) }], "name" => file.filename, @@ -53,4 +60,34 @@ defmodule Pleroma.Upload do defp url_for(file) do "#{Web.base_url()}/media/#{file}" end + + def get_content_type(file) do + match = File.open(file, [:read], fn(f) -> + case IO.binread(f, 8) do + <<0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a>> -> + "image/png" + <<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> -> + "image/gif" + <<0xff, 0xd8, 0xff, _, _, _, _, _>> -> + "image/jpeg" + <<0x1a, 0x45, 0xdf, 0xa3, _, _, _, _>> -> + "video/webm" + <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> -> + "video/mp4" + <<0x49, 0x44, 0x33, _, _, _, _, _>> -> + "audio/mpeg" + <<0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> -> + "audio/ogg" + <<0x52, 0x49, 0x46, 0x46, _, _, _, _>> -> + "audio/wav" + _ -> + "application/octet-stream" + end + end) + + case match do + {:ok, type} -> type + _e -> "application/octet-stream" + end + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1624c6545..35536a1e4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -22,6 +22,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional), {:ok, activity} <- insert(create_data, local), :ok <- maybe_federate(activity) do + if activity.data["type"] == "Create" and Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do + Pleroma.Web.Streamer.stream("public", activity) + end {:ok, activity} end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index a1b4108cd..dc1ba2a05 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -2,6 +2,7 @@ defmodule Pleroma.Web.Endpoint do use Phoenix.Endpoint, otp_app: :pleroma socket "/socket", Pleroma.Web.UserSocket + socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket # Serve at "/" the static files from "priv/static" directory. # diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index d95b18315..9f50dc596 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.Web.CommonAPI + alias Pleroma.Web.{CommonAPI, OStatus} import Ecto.Query import Logger @@ -23,6 +23,57 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def update_credentials(%{assigns: %{user: user}} = conn, params) do + params = if bio = params["note"] do + Map.put(params, "bio", bio) + else + params + end + + params = if name = params["display_name"] do + Map.put(params, "name", name) + else + params + end + + user = if avatar = params["avatar"] do + with %Plug.Upload{} <- avatar, + {:ok, object} <- ActivityPub.upload(avatar), + change = Ecto.Changeset.change(user, %{avatar: object.data}), + {:ok, user} = Repo.update(change) do + user + else + _e -> user + end + else + user + end + + user = if banner = params["header"] do + with %Plug.Upload{} <- banner, + {:ok, object} <- ActivityPub.upload(banner), + new_info <- Map.put(user.info, "banner", object.data), + change <- User.info_changeset(user, %{info: new_info}), + {:ok, user} <- Repo.update(change) do + user + else + _e -> user + end + else + user + end + + with changeset <- User.update_changeset(user, params), + {:ok, user} <- Repo.update(changeset) do + json conn, AccountView.render("account.json", %{user: user}) + else + _e -> + conn + |> put_status(403) + |> json(%{error: "Invalid request"}) + end + end + def verify_credentials(%{assigns: %{user: user}} = conn, params) do account = AccountView.render("account.json", %{user: user}) json(conn, account) @@ -373,11 +424,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do accounts = User.search(query, params["resolve"] == "true") + fetched = if Regex.match?(~r/https?:/, query) do + with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do + activities + else + _e -> [] + end + end || [] + q = from a in Activity, where: fragment("?->>'type' = 'Create'", a.data), where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query), limit: 20 - statuses = Repo.all(q) + statuses = Repo.all(q) ++ fetched res = %{ "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex new file mode 100644 index 000000000..f9c8cec32 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex @@ -0,0 +1,34 @@ +defmodule Pleroma.Web.MastodonAPI.MastodonSocket do + use Phoenix.Socket + + transport :streaming, Phoenix.Transports.WebSocket.Raw, + timeout: :infinity # We never receive data. + + def connect(params, socket) do + if params["stream"] == "public" do + socket = socket + |> assign(:topic, params["stream"]) + Pleroma.Web.Streamer.add_socket(params["stream"], socket) + {:ok, socket} + else + :error + end + end + + def id(socket), do: nil + + def handle(:text, message, state) do + IO.inspect message + #| :ok + #| state + #| {:text, message} + #| {:text, message, state} + #| {:close, "Goodbye!"} + {:text, message} + end + + def handle(:closed, reason, %{socket: socket}) do + topic = socket.assigns[:topic] + Pleroma.Web.Streamer.remove_socket(topic, socket) + end +end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index cf97ab746..16322cf21 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do header = image_url(user.info["banner"]) || "https://placehold.it/700x335" %{ - id: user.id, + id: to_string(user.id), username: hd(String.split(user.nickname, "@")), acct: user.nickname, display_name: user.name, @@ -43,7 +43,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("mention.json", %{user: user}) do %{ - id: user.id, + id: to_string(user.id), acct: user.nickname, username: hd(String.split(user.nickname, "@")), url: user.ap_id @@ -52,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do def render("relationship.json", %{user: user, target: target}) do %{ - id: target.id, + id: to_string(target.id), following: User.following?(user, target), followed_by: User.following?(target, user), blocking: User.blocks?(user, target), diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 09a2ca404..d97b2acb4 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -45,7 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do name: "Web", website: nil }, - language: nil + language: nil, + emojis: [] } end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 841df8c32..e8483dec0 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -25,7 +25,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do auth: auth } else - url = "#{redirect_uri}?code=#{auth.token}" + connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" + url = "#{redirect_uri}#{connector}code=#{auth.token}" url = if params["state"] do url <> "&state=#{params["state"]}" else diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index efd37ede2..637c300c8 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -53,6 +53,7 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through :authenticated_api + patch "/accounts/update_credentials", MastodonAPIController, :update_credentials get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials get "/accounts/relationships", MastodonAPIController, :relationships get "/accounts/search", MastodonAPIController, :account_search diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex new file mode 100644 index 000000000..3a7b91743 --- /dev/null +++ b/lib/pleroma/web/streamer.ex @@ -0,0 +1,76 @@ +defmodule Pleroma.Web.Streamer do + use GenServer + require Logger + import Plug.Conn + + def start_link do + spawn(fn -> + Process.sleep(1000 * 30) # 30 seconds + GenServer.cast(__MODULE__, %{action: :ping}) + end) + GenServer.start_link(__MODULE__, %{}, name: __MODULE__) + end + + def add_socket(topic, socket) do + GenServer.cast(__MODULE__, %{action: :add, socket: socket, topic: topic}) + end + + def remove_socket(topic, socket) do + GenServer.cast(__MODULE__, %{action: :remove, socket: socket, topic: topic}) + end + + def stream(topic, item) do + GenServer.cast(__MODULE__, %{action: :stream, topic: topic, item: item}) + end + + def handle_cast(%{action: :ping}, topics) do + Map.values(topics) + |> List.flatten + |> Enum.each(fn (socket) -> + Logger.debug("Sending keepalive ping") + send socket.transport_pid, {:text, ""} + end) + spawn(fn -> + Process.sleep(1000 * 30) # 30 seconds + GenServer.cast(__MODULE__, %{action: :ping}) + end) + {:noreply, topics} + end + + def handle_cast(%{action: :stream, topic: topic, item: item}, topics) do + Logger.debug("Trying to push to #{topic}") + Logger.debug("Pushing item to #{topic}") + Enum.each(topics[topic] || [], fn (socket) -> + json = %{ + event: "update", + payload: Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: item) |> Poison.encode! + } |> Poison.encode! + + send socket.transport_pid, {:text, json} + end) + {:noreply, topics} + end + + def handle_cast(%{action: :add, topic: topic, socket: socket}, sockets) do + sockets_for_topic = sockets[topic] || [] + sockets_for_topic = Enum.uniq([socket | sockets_for_topic]) + sockets = Map.put(sockets, topic, sockets_for_topic) + Logger.debug("Got new conn for #{topic}") + IO.inspect(sockets) + {:noreply, sockets} + end + + def handle_cast(%{action: :remove, topic: topic, socket: socket}, sockets) do + sockets_for_topic = sockets[topic] || [] + sockets_for_topic = List.delete(sockets_for_topic, socket) + sockets = Map.put(sockets, topic, sockets_for_topic) + Logger.debug("Removed conn for #{topic}") + IO.inspect(sockets) + {:noreply, sockets} + end + + def handle_cast(m, state) do + IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}") + {:noreply, state} + end +end diff --git a/lib/transports.ex b/lib/transports.ex new file mode 100644 index 000000000..5600a4fdd --- /dev/null +++ b/lib/transports.ex @@ -0,0 +1,77 @@ +defmodule Phoenix.Transports.WebSocket.Raw do + import Plug.Conn, only: [ + fetch_query_params: 1, + send_resp: 3 + ] + alias Phoenix.Socket.Transport + + def default_config do + [ + timeout: 60_000, + transport_log: false, + cowboy: Phoenix.Endpoint.CowboyWebSocket + ] + end + + def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do + {_, opts} = handler.__transport__(transport) + + conn = conn + |> fetch_query_params + |> Transport.transport_log(opts[:transport_log]) + |> Transport.force_ssl(handler, endpoint, opts) + |> Transport.check_origin(handler, endpoint, opts) + + case conn do + %{halted: false} = conn -> + case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + {:ok, socket} -> + {:ok, conn, {__MODULE__, {socket, opts}}} + :error -> + send_resp(conn, :forbidden, "") + {:error, conn} + end + _ -> + {:error, conn} + end + end + + def init(conn, _) do + send_resp(conn, :bad_request, "") + {:error, conn} + end + + def ws_init({socket, config}) do + Process.flag(:trap_exit, true) + {:ok, %{socket: socket}, config[:timeout]} + end + + def ws_handle(op, data, state) do + state.socket.handler + |> apply(:handle, [op, data, state]) + |> case do + {op, data} -> + {:reply, {op, data}, state} + {op, data, state} -> + {:reply, {op, data}, state} + %{} = state -> + {:ok, state} + _ -> + {:ok, state} + end + end + + def ws_info({op, data} = tuple, state) do + {:reply, tuple, state} + end + + def ws_info(_tuple, state), do: {:ok, state} + + def ws_close(state) do + ws_handle(:closed, :normal, state) + end + + def ws_terminate(reason, state) do + ws_handle(:closed, reason, state) + end +end |