From 4e785df984bed0e2ffc3f5a773a961ed3efd4760 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Wed, 6 Sep 2017 19:05:35 +0200 Subject: [PATCH 01/26] Update Phoenix, add Phoenix.HTML. --- mix.exs | 3 ++- mix.lock | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index f5457ee08..1722f616c 100644 --- a/mix.exs +++ b/mix.exs @@ -28,7 +28,7 @@ defp elixirc_paths(_), do: ["lib"] # # Type `mix help deps` for examples and options. defp deps do - [{:phoenix, "~> 1.3.0-rc"}, + [{:phoenix, "~> 1.3.0"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.2"}, {:postgrex, ">= 0.0.0"}, @@ -37,6 +37,7 @@ defp deps do {:comeonin, "~> 3.0"}, {:trailing_format_plug, "~> 0.0.5" }, {:html_sanitize_ex, "~> 1.3.0-rc1"}, + {:phoenix_html, "~> 2.10"}, {:calendar, "~> 0.16.1"}, {:cachex, "~> 2.1"}, {:httpoison, "~> 0.11.2"}, diff --git a/mix.lock b/mix.lock index e43463aef..ac9ec5384 100644 --- a/mix.lock +++ b/mix.lock @@ -27,10 +27,11 @@ "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, "mix_test_watch": {:hex, :mix_test_watch, "0.3.3", "70859889a8d1d43d1b75d69d87258a301f43209a17787cdb2bd9cab42adf271d", [:mix], [{:fs, "~> 2.12", [hex: :fs, optional: false]}]}, "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], []}, - "phoenix": {:hex, :phoenix, "1.3.0-rc.1", "0d04948a4bd24823f101024c07b6a4d35e58f1fd92a465c1bc75dd37acd1041a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]}, + "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.2.3", "450c749876ff1de4a78fdb305a142a76817c77a1cd79aeca29e5fc9a6c630b26", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, - "plug": {:hex, :plug, "1.3.4", "b4ef3a383f991bfa594552ded44934f2a9853407899d47ecc0481777fb1906f6", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, + "phoenix_html": {:hex, :phoenix_html, "2.10.4", "d4f99c32d5dc4918b531fdf163e1fd7cf20acdd7703f16f5d02d4db36de803b7", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], []}, + "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, "postgrex": {:hex, :postgrex, "0.13.2", "2b88168fc6a5456a27bfb54ccf0ba4025d274841a7a3af5e5deb1b755d95154e", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, From 2a298d70f9938d1b6d5af04d8b8863fdd3299f46 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Wed, 6 Sep 2017 19:06:25 +0200 Subject: [PATCH 02/26] Add very basic oauth and mastodon api support. --- lib/pleroma/app.ex | 29 ++++++++++++ lib/pleroma/plugs/oauth_plug.ex | 22 ++++++++++ lib/pleroma/web/mastodon_api/mastodon_api.ex | 0 .../mastodon_api/mastodon_api_controller.ex | 32 ++++++++++++++ lib/pleroma/web/oauth/authorization.ex | 30 +++++++++++++ lib/pleroma/web/oauth/oauth_controller.ex | 44 +++++++++++++++++++ lib/pleroma/web/oauth/oauth_view.ex | 4 ++ lib/pleroma/web/oauth/token.ex | 31 +++++++++++++ lib/pleroma/web/router.ex | 18 ++++++++ lib/pleroma/web/templates/layout/app.html.eex | 11 +++++ .../templates/o_auth/o_auth/results.html.eex | 2 + .../web/templates/o_auth/o_auth/show.html.eex | 14 ++++++ lib/pleroma/web/views/layout_view.ex | 3 ++ .../20170906120646_add_mastodon_apps.exs | 16 +++++++ ...906143140_create_o_auth_authorizations.exs | 15 +++++++ .../20170906152508_create_o_auth_token.exs | 15 +++++++ 16 files changed, 286 insertions(+) create mode 100644 lib/pleroma/app.ex create mode 100644 lib/pleroma/plugs/oauth_plug.ex create mode 100644 lib/pleroma/web/mastodon_api/mastodon_api.ex create mode 100644 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex create mode 100644 lib/pleroma/web/oauth/authorization.ex create mode 100644 lib/pleroma/web/oauth/oauth_controller.ex create mode 100644 lib/pleroma/web/oauth/oauth_view.ex create mode 100644 lib/pleroma/web/oauth/token.ex create mode 100644 lib/pleroma/web/templates/layout/app.html.eex create mode 100644 lib/pleroma/web/templates/o_auth/o_auth/results.html.eex create mode 100644 lib/pleroma/web/templates/o_auth/o_auth/show.html.eex create mode 100644 lib/pleroma/web/views/layout_view.ex create mode 100644 priv/repo/migrations/20170906120646_add_mastodon_apps.exs create mode 100644 priv/repo/migrations/20170906143140_create_o_auth_authorizations.exs create mode 100644 priv/repo/migrations/20170906152508_create_o_auth_token.exs diff --git a/lib/pleroma/app.ex b/lib/pleroma/app.ex new file mode 100644 index 000000000..d467595ea --- /dev/null +++ b/lib/pleroma/app.ex @@ -0,0 +1,29 @@ +defmodule Pleroma.App do + use Ecto.Schema + import Ecto.{Changeset} + + schema "apps" do + field :client_name, :string + field :redirect_uris, :string + field :scopes, :string + field :website, :string + field :client_id, :string + field :client_secret, :string + + timestamps() + end + + def register_changeset(struct, params \\ %{}) do + changeset = struct + |> cast(params, [:client_name, :redirect_uris, :scopes, :website]) + |> validate_required([:client_name, :redirect_uris, :scopes]) + + if changeset.valid? do + changeset + |> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64) + |> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64) + else + changeset + end + end +end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex new file mode 100644 index 000000000..fc2a907a2 --- /dev/null +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -0,0 +1,22 @@ +defmodule Pleroma.Plugs.OAuthPlug do + import Plug.Conn + alias Pleroma.User + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + def call(conn, opts) do + with ["Bearer " <> header] <- get_req_header(conn, "authorization"), + %Token{user_id: user_id} <- Repo.get_by(Token, token: header), + %User{} = user <- Repo.get(User, user_id) do + conn + |> assign(:user, user) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex new file mode 100644 index 000000000..89e37d6ab --- /dev/null +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do + use Pleroma.Web, :controller + alias Pleroma.{Repo, App} + + def create_app(conn, params) do + with cs <- App.register_changeset(%App{}, params) |> IO.inspect, + {:ok, app} <- Repo.insert(cs) |> IO.inspect do + res = %{ + id: app.id, + client_id: app.client_id, + client_secret: app.client_secret + } + + json(conn, res) + end + end + + def verify_credentials(%{assigns: %{user: user}} = conn, params) do + account = %{ + id: user.id, + username: user.nickname, + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: user.inserted_at, + note: user.bio, + url: "" + } + + json(conn, account) + end +end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex new file mode 100644 index 000000000..9423c9632 --- /dev/null +++ b/lib/pleroma/web/oauth/authorization.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Web.OAuth.Authorization do + use Ecto.Schema + + alias Pleroma.{App, User, Repo} + alias Pleroma.Web.OAuth.Authorization + + schema "oauth_authorizations" do + field :token, :string + field :valid_until, :naive_datetime + field :used, :boolean, default: false + belongs_to :user, Pleroma.User + belongs_to :app, Pleroma.App + + timestamps() + end + + def create_authorization(%App{} = app, %User{} = user) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 + + authorization = %Authorization{ + token: token, + used: false, + user_id: user.id, + app_id: app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 60 * 10) + } + + Repo.insert(authorization) + end +end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex new file mode 100644 index 000000000..f0e091ac2 --- /dev/null +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -0,0 +1,44 @@ +defmodule Pleroma.Web.OAuth.OAuthController do + use Pleroma.Web, :controller + + alias Pleroma.Web.OAuth.{Authorization, Token} + alias Pleroma.{Repo, User, App} + alias Comeonin.Pbkdf2 + + def authorize(conn, params) do + render conn, "show.html", %{ + response_type: params["response_type"], + client_id: params["client_id"], + scope: params["scope"], + redirect_uri: params["redirect_uri"] + } + end + + def create_authorization(conn, %{"authorization" => %{"name" => name, "password" => password, "client_id" => client_id}} = params) do + with %User{} = user <- User.get_cached_by_nickname(name), + true <- Pbkdf2.checkpw(password, user.password_hash), + %App{} = app <- Pleroma.Repo.get_by(Pleroma.App, client_id: client_id), + {:ok, auth} <- Authorization.create_authorization(app, user) do + render conn, "results.html", %{ + auth: auth + } + end + end + + # TODO CRITICAL + # - Check validity of auth token + def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do + with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]), + %Authorization{} = auth <- Repo.get_by(Authorization, token: params["code"], app_id: app.id), + {:ok, token} <- Token.create_token(app, Repo.get(User, auth.user_id)) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: 60 * 10, + scope: "read write follow" + } + json(conn, response) + end + end +end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex new file mode 100644 index 000000000..b3923fcf5 --- /dev/null +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -0,0 +1,4 @@ +defmodule Pleroma.Web.OAuth.OAuthView do + use Pleroma.Web, :view + import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex new file mode 100644 index 000000000..49e72428c --- /dev/null +++ b/lib/pleroma/web/oauth/token.ex @@ -0,0 +1,31 @@ +defmodule Pleroma.Web.OAuth.Token do + use Ecto.Schema + + alias Pleroma.{App, User, Repo} + alias Pleroma.Web.OAuth.Token + + schema "oauth_tokens" do + field :token, :string + field :refresh_token, :string + field :valid_until, :naive_datetime + belongs_to :user, Pleroma.User + belongs_to :app, Pleroma.App + + timestamps() + end + + def create_token(%App{} = app, %User{} = user) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 + refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 + + token = %Token{ + token: token, + refresh_token: refresh_token, + user_id: user.id, + app_id: app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 60 * 10) + } + + Repo.insert(token) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c20ec3e80..6081016d6 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -16,6 +16,7 @@ def user_fetcher(username) do pipeline :authenticated_api do plug :accepts, ["json"] plug :fetch_session + plug Pleroma.Plugs.OAuthPlug plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1} end @@ -31,10 +32,27 @@ def user_fetcher(username) do plug :accepts, ["json"] end + pipeline :oauth do + plug :accepts, ["html", "json"] + end + + scope "/oauth", Pleroma.Web.OAuth do + get "/authorize", OAuthController, :authorize + post "/authorize", OAuthController, :create_authorization + post "/token", OAuthController, :token_exchange + end + scope "/api/v1", Pleroma.Web do pipe_through :masto_config # TODO: Move this get "/instance", TwitterAPI.UtilController, :masto_instance + post "/apps", MastodonAPI.MastodonAPIController, :create_app + end + + scope "/api/v1", Pleroma.Web.MastodonAPI do + pipe_through :authenticated_api + + get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex new file mode 100644 index 000000000..6cc3b7ac5 --- /dev/null +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -0,0 +1,11 @@ + + + + + Pleroma + + +

Welcome to Pleroma

+ <%= render @view_module, @view_template, assigns %> + + diff --git a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex new file mode 100644 index 000000000..8443d906b --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex @@ -0,0 +1,2 @@ +

Successfully authorized

+

Token code is <%= @auth.token %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex new file mode 100644 index 000000000..ce295ed05 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -0,0 +1,14 @@ +

OAuth Authorization

+<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> +<%= label f, :name, "Name" %> +<%= text_input f, :name %> +
+<%= label f, :password, "Password" %> +<%= password_input f, :password %> +
+<%= hidden_input f, :client_id, value: @client_id %> +<%= hidden_input f, :response_type, value: @response_type %> +<%= hidden_input f, :redirect_uri, value: @redirect_uri %> +<%= hidden_input f, :scope, value: @scope %> +<%= submit "Authorize" %> +<% end %> diff --git a/lib/pleroma/web/views/layout_view.ex b/lib/pleroma/web/views/layout_view.ex new file mode 100644 index 000000000..d4d4c3bd3 --- /dev/null +++ b/lib/pleroma/web/views/layout_view.ex @@ -0,0 +1,3 @@ +defmodule Pleroma.Web.LayoutView do + use Pleroma.Web, :view +end diff --git a/priv/repo/migrations/20170906120646_add_mastodon_apps.exs b/priv/repo/migrations/20170906120646_add_mastodon_apps.exs new file mode 100644 index 000000000..d3dd317dd --- /dev/null +++ b/priv/repo/migrations/20170906120646_add_mastodon_apps.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.AddMastodonApps do + use Ecto.Migration + + def change do + create table(:apps) do + add :client_name, :string + add :redirect_uris, :string + add :scopes, :string + add :website, :string + add :client_id, :string + add :client_secret, :string + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20170906143140_create_o_auth_authorizations.exs b/priv/repo/migrations/20170906143140_create_o_auth_authorizations.exs new file mode 100644 index 000000000..b4332870e --- /dev/null +++ b/priv/repo/migrations/20170906143140_create_o_auth_authorizations.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateOAuthAuthorizations do + use Ecto.Migration + + def change do + create table(:oauth_authorizations) do + add :app_id, references(:apps) + add :user_id, references(:users) + add :token, :string + add :valid_until, :naive_datetime + add :used, :boolean, default: false + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20170906152508_create_o_auth_token.exs b/priv/repo/migrations/20170906152508_create_o_auth_token.exs new file mode 100644 index 000000000..7f8550f33 --- /dev/null +++ b/priv/repo/migrations/20170906152508_create_o_auth_token.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateOAuthToken do + use Ecto.Migration + + def change do + create table(:oauth_tokens) do + add :app_id, references(:apps) + add :user_id, references(:users) + add :token, :string + add :refresh_token, :string + add :valid_until, :naive_datetime + + timestamps() + end + end +end From 2652d9e4edf34531057d472c6e23812873019fd5 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 7 Sep 2017 08:58:10 +0200 Subject: [PATCH 03/26] Slight cleanup. --- .../mastodon_api/mastodon_api_controller.ex | 28 +++++++++++-------- .../web/mastodon_api/views/user_view.ex | 27 ++++++++++++++++++ lib/pleroma/{ => web/oauth}/app.ex | 2 +- lib/pleroma/web/oauth/authorization.ex | 4 +-- lib/pleroma/web/oauth/oauth_controller.ex | 6 ++-- lib/pleroma/web/oauth/token.ex | 4 +-- lib/pleroma/web/router.ex | 13 +++------ .../controllers/util_controller.ex | 12 -------- 8 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 lib/pleroma/web/mastodon_api/views/user_view.ex rename lib/pleroma/{ => web/oauth}/app.ex (95%) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 89e37d6ab..62522439c 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,6 +1,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo, App} + alias Pleroma.{Repo} + alias Pleroma.Web.OAuth.App + alias Pleroma.Web + alias Pleroma.Web.MastodonAPI.AccountView def create_app(conn, params) do with cs <- App.register_changeset(%App{}, params) |> IO.inspect, @@ -16,17 +19,18 @@ def create_app(conn, params) do end def verify_credentials(%{assigns: %{user: user}} = conn, params) do - account = %{ - id: user.id, - username: user.nickname, - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: user.inserted_at, - note: user.bio, - url: "" - } - + account = AccountView.render("account.json", %{user: user}) json(conn, account) end + + def masto_instance(conn, _params) do + response = %{ + uri: Web.base_url, + title: Web.base_url, + description: "A Pleroma instance, an alternative fediverse server", + version: "Pleroma Dev" + } + + json(conn, response) + end end diff --git a/lib/pleroma/web/mastodon_api/views/user_view.ex b/lib/pleroma/web/mastodon_api/views/user_view.ex new file mode 100644 index 000000000..88e32d6f9 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/user_view.ex @@ -0,0 +1,27 @@ +defmodule Pleroma.Web.MastodonAPI.AccountView do + use Pleroma.Web, :view + alias Pleroma.User + + def render("account.json", %{user: user}) do + image = User.avatar_url(user) + user_info = User.user_info(user) + + %{ + id: user.id, + username: user.nickname, + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: user.inserted_at, + followers_count: user_info.follower_count, + following_count: user_info.following_count, + statuses_count: user_info.note_count, + note: user.bio, + url: user.ap_id, + avatar: image, + avatar_static: image, + header: "", + header_static: "" + } + end +end diff --git a/lib/pleroma/app.ex b/lib/pleroma/web/oauth/app.ex similarity index 95% rename from lib/pleroma/app.ex rename to lib/pleroma/web/oauth/app.ex index d467595ea..ff52ba82e 100644 --- a/lib/pleroma/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -1,4 +1,4 @@ -defmodule Pleroma.App do +defmodule Pleroma.Web.OAuth.App do use Ecto.Schema import Ecto.{Changeset} diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 9423c9632..c47289455 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -1,8 +1,8 @@ defmodule Pleroma.Web.OAuth.Authorization do use Ecto.Schema - alias Pleroma.{App, User, Repo} - alias Pleroma.Web.OAuth.Authorization + alias Pleroma.{User, Repo} + alias Pleroma.Web.OAuth.{Authorization, App} schema "oauth_authorizations" do field :token, :string diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index f0e091ac2..a6a411573 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -1,8 +1,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller - alias Pleroma.Web.OAuth.{Authorization, Token} - alias Pleroma.{Repo, User, App} + alias Pleroma.Web.OAuth.{Authorization, Token, App} + alias Pleroma.{Repo, User} alias Comeonin.Pbkdf2 def authorize(conn, params) do @@ -17,7 +17,7 @@ def authorize(conn, params) do def create_authorization(conn, %{"authorization" => %{"name" => name, "password" => password, "client_id" => client_id}} = params) do with %User{} = user <- User.get_cached_by_nickname(name), true <- Pbkdf2.checkpw(password, user.password_hash), - %App{} = app <- Pleroma.Repo.get_by(Pleroma.App, client_id: client_id), + %App{} = app <- Repo.get_by(App, client_id: client_id), {:ok, auth} <- Authorization.create_authorization(app, user) do render conn, "results.html", %{ auth: auth diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 49e72428c..da723d6d6 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -1,8 +1,8 @@ defmodule Pleroma.Web.OAuth.Token do use Ecto.Schema - alias Pleroma.{App, User, Repo} - alias Pleroma.Web.OAuth.Token + alias Pleroma.{User, Repo} + alias Pleroma.Web.OAuth.{Token, App} schema "oauth_tokens" do field :token, :string diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6081016d6..a8577c30b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -28,10 +28,6 @@ def user_fetcher(username) do plug :accepts, ["json", "xml"] end - pipeline :masto_config do - plug :accepts, ["json"] - end - pipeline :oauth do plug :accepts, ["html", "json"] end @@ -42,11 +38,10 @@ def user_fetcher(username) do post "/token", OAuthController, :token_exchange end - scope "/api/v1", Pleroma.Web do - pipe_through :masto_config - # TODO: Move this - get "/instance", TwitterAPI.UtilController, :masto_instance - post "/apps", MastodonAPI.MastodonAPIController, :create_app + scope "/api/v1", Pleroma.Web.MastodonAPI do + pipe_through :api + get "/instance", MastodonAPO.Controller, :masto_instance + post "/apps", MastodonAPIController, :create_app end scope "/api/v1", Pleroma.Web.MastodonAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 285b4d105..41881e742 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -42,16 +42,4 @@ def version(conn, _params) do _ -> json(conn, "Pleroma Dev") end end - - # TODO: Move this - def masto_instance(conn, _params) do - response = %{ - uri: Web.base_url, - title: Web.base_url, - description: "A Pleroma instance, an alternative fediverse server", - version: "dev" - } - - json(conn, response) - end end From 890503ca1ea4308664d31622eb19208757a4c881 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:00:03 +0200 Subject: [PATCH 04/26] Remove mix test.watch It recompiled too often and tested too long. --- mix.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 1722f616c..00733c26a 100644 --- a/mix.exs +++ b/mix.exs @@ -42,8 +42,7 @@ defp deps do {:cachex, "~> 2.1"}, {:httpoison, "~> 0.11.2"}, {:ex_machina, "~> 2.0", only: :test}, - {:credo, "~> 0.7", only: [:dev, :test]}, - {:mix_test_watch, "~> 0.2", only: :dev}] + {:credo, "~> 0.7", only: [:dev, :test]}] end # Aliases are shortcuts or tasks specific to the current project. From 95cedd60004893fd646735d17f7196297c38e22c Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:02:59 +0200 Subject: [PATCH 05/26] Make auth tokens usable once and expire them. --- lib/pleroma/web/oauth/authorization.ex | 17 +++++++++++ lib/pleroma/web/oauth/token.ex | 9 +++++- test/web/oauth/authorization_test.exs | 42 ++++++++++++++++++++++++++ test/web/oauth/token_test.exs | 24 +++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/web/oauth/authorization_test.exs create mode 100644 test/web/oauth/token_test.exs diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index c47289455..1ba5be602 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.OAuth.Authorization do alias Pleroma.{User, Repo} alias Pleroma.Web.OAuth.{Authorization, App} + import Ecto.{Changeset} + schema "oauth_authorizations" do field :token, :string field :valid_until, :naive_datetime @@ -27,4 +29,19 @@ def create_authorization(%App{} = app, %User{} = user) do Repo.insert(authorization) end + + def use_changeset(%Authorization{} = auth, params) do + auth + |> cast(params, [:used]) + |> validate_required([:used]) + end + + def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do + if NaiveDateTime.diff(NaiveDateTime.utc_now, valid_until) < 0 do + Repo.update(use_changeset(auth, %{used: true})) + else + {:error, "token expired"} + end + end + def use_token(%Authorization{used: true}), do: {:error, "already used"} end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index da723d6d6..828a966fb 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -2,7 +2,7 @@ defmodule Pleroma.Web.OAuth.Token do use Ecto.Schema alias Pleroma.{User, Repo} - alias Pleroma.Web.OAuth.{Token, App} + alias Pleroma.Web.OAuth.{Token, App, Authorization} schema "oauth_tokens" do field :token, :string @@ -14,6 +14,13 @@ defmodule Pleroma.Web.OAuth.Token do timestamps() end + def exchange_token(app, auth) do + with {:ok, auth} <- Authorization.use_token(auth), + true <- auth.app_id == app.id do + create_token(app, Repo.get(User, auth.user_id)) + end + end + def create_token(%App{} = app, %User{} = user) do token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs new file mode 100644 index 000000000..52441fa7d --- /dev/null +++ b/test/web/oauth/authorization_test.exs @@ -0,0 +1,42 @@ +defmodule Pleroma.Web.OAuth.AuthorizationTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.{Authorization, App} + import Pleroma.Factory + + test "create an authorization token for a valid app" do + {:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + + assert auth.user_id == user.id + assert auth.app_id == app.id + assert String.length(auth.token) > 10 + assert auth.used == false + end + + test "use up a token" do + {:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + + {:ok, auth} = Authorization.use_token(auth) + + assert auth.used == true + + assert {:error, "already used"} == Authorization.use_token(auth) + + expired_auth = %Authorization{ + user_id: user.id, + app_id: app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, -10), + token: "mytoken", + used: false + } + + {:ok, expired_auth} = Repo.insert(expired_auth) + + assert {:error, "token expired"} == Authorization.use_token(expired_auth) + end +end diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs new file mode 100644 index 000000000..3bd763989 --- /dev/null +++ b/test/web/oauth/token_test.exs @@ -0,0 +1,24 @@ +defmodule Pleroma.Web.OAuth.TokenTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.{App, Token, Authorization} + alias Pleroma.Repo + + import Pleroma.Factory + + test "exchanges a auth token for an access token" do + {:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + + {:ok, token} = Token.exchange_token(app, auth) + + assert token.app_id == app.id + assert token.user_id == user.id + assert String.length(token.token) > 10 + assert String.length(token.refresh_token) > 10 + + auth = Repo.get(Authorization, auth.id) + {:error, "already used"} = Token.exchange_token(app, auth) + end +end From a22f2e683b5e77eb563f0ca05a2160578ed2ac82 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:05:17 +0200 Subject: [PATCH 06/26] Add type restriction to activitypub fetcher Mainly because Mastodon only returns notes, not the other activities. --- lib/pleroma/web/activity_pub/activity_pub.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index db1302738..8ae321658 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -133,6 +133,12 @@ defp restrict_actor(query, %{"actor_id" => actor_id}) do end defp restrict_actor(query, _), do: query + defp restrict_type(query, %{"type" => type}) do + from activity in query, + where: fragment("?->>'type' = ?", activity.data, ^type) + end + defp restrict_type(query, _), do: query + def fetch_activities(recipients, opts \\ %{}) do base_query = from activity in Activity, limit: 20, @@ -144,6 +150,7 @@ def fetch_activities(recipients, opts \\ %{}) do |> restrict_local(opts) |> restrict_max(opts) |> restrict_actor(opts) + |> restrict_type(opts) |> Repo.all |> Enum.reverse end From c6bdc5960c4dbbdd5d5d86b6d49669611392c73f Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:09:53 +0200 Subject: [PATCH 07/26] Test for Mastodon AccountView Handles users and mentions. --- .../views/{user_view.ex => account_view.ex} | 9 ++++ test/web/mastodon_api/account_view_test.exs | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) rename lib/pleroma/web/mastodon_api/views/{user_view.ex => account_view.ex} (80%) create mode 100644 test/web/mastodon_api/account_view_test.exs diff --git a/lib/pleroma/web/mastodon_api/views/user_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex similarity index 80% rename from lib/pleroma/web/mastodon_api/views/user_view.ex rename to lib/pleroma/web/mastodon_api/views/account_view.ex index 88e32d6f9..5f6ca84d0 100644 --- a/lib/pleroma/web/mastodon_api/views/user_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -24,4 +24,13 @@ def render("account.json", %{user: user}) do header_static: "" } end + + def render("mention.json", %{user: user}) do + %{ + id: user.id, + acct: user.nickname, + username: user.nickname, + url: user.ap_id + } + end end diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs new file mode 100644 index 000000000..f0c8673ad --- /dev/null +++ b/test/web/mastodon_api/account_view_test.exs @@ -0,0 +1,42 @@ +defmodule Pleroma.Web.MastodonAPI.AccountViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.AccountView + + test "Represent a user account" do + user = insert(:user, %{info: %{"note_count" => 5, "follower_count" => 3}}) + + expected = %{ + id: user.id, + username: user.nickname, + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: user.inserted_at, + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "https://placehold.it/48x48", + avatar_static: "https://placehold.it/48x48", + header: "", + header_static: "" + } + + assert expected == AccountView.render("account.json", %{user: user}) + end + + test "Represent a smaller mention" do + user = insert(:user) + + expected = %{ + id: user.id, + acct: user.nickname, + username: user.nickname, + url: user.ap_id + } + + assert expected == AccountView.render("mention.json", %{user: user}) + end +end From 2b7efff71bc6a59f235de9cfea0ad244f201ba25 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:10:29 +0200 Subject: [PATCH 08/26] Add Mastodon StatusView. --- .../web/mastodon_api/views/status_view.ex | 49 +++++++++++++++++ test/web/mastodon_api/status_view_test.exs | 53 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 lib/pleroma/web/mastodon_api/views/status_view.ex create mode 100644 test/web/mastodon_api/status_view_test.exs diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex new file mode 100644 index 000000000..45e7d45f4 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -0,0 +1,49 @@ +defmodule Pleroma.Web.MastodonAPI.StatusView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI.{AccountView, StatusView} + alias Pleroma.User + + def render("index.json", opts) do + render_many(opts.activities, StatusView, "status.json", opts) + end + + def render("status.json", %{activity: %{data: %{"object" => object}} = activity}) do + user = User.get_cached_by_ap_id(activity.data["actor"]) + + like_count = object["like_count"] || 0 + announcement_count = object["announcement_count"] || 0 + + tags = object["tag"] || [] + sensitive = Enum.member?(tags, "nsfw") + + mentions = activity.data["to"] + |> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) + |> Enum.filter(&(&1)) + |> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) + + %{ + id: activity.id, + uri: object["id"], + url: object["external_url"], + account: AccountView.render("account.json", %{user: user}), + in_reply_to_id: object["inReplyToStatusId"], + in_reply_to_account_id: nil, + reblog: nil, + content: HtmlSanitizeEx.basic_html(object["content"]), + created_at: object["published"], + reblogs_count: announcement_count, + favourites_count: like_count, + reblogged: false, + favourited: false, # fix + muted: false, + sensitive: sensitive, + spoiler_text: "", + visibility: "public", + media_attachments: [], # fix + mentions: mentions, + tags: [], # fix, + application: nil, + language: nil + } + end +end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs new file mode 100644 index 000000000..b8a96f71a --- /dev/null +++ b/test/web/mastodon_api/status_view_test.exs @@ -0,0 +1,53 @@ +defmodule Pleroma.Web.MastodonAPI.StatusViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} + alias Pleroma.User + alias Pleroma.Web.OStatus + import Pleroma.Factory + + test "a note activity" do + note = insert(:note_activity) + user = User.get_cached_by_ap_id(note.data["actor"]) + + status = StatusView.render("status.json", %{activity: note}) + + expected = %{ + id: note.id, + uri: note.data["object"]["id"], + url: note.data["object"]["external_id"], + account: AccountView.render("account.json", %{user: user}), + in_reply_to_id: nil, + in_reply_to_account_id: nil, + reblog: nil, + content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]), + created_at: note.data["object"]["published"], + reblogs_count: 0, + favourites_count: 0, + reblogged: false, + favourited: false, + muted: false, + sensitive: false, + spoiler_text: "", + visibility: "public", + media_attachments: [], + mentions: [], + tags: [], + application: nil, + language: nil + } + + assert status == expected + end + + test "contains mentions" do + incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") + user = insert(:user, %{ap_id: "https://pleroma.soykaf.com/users/lain"}) + + {:ok, [activity]} = OStatus.handle_incoming(incoming) + + status = StatusView.render("status.json", %{activity: activity}) + + assert status.mentions == [AccountView.render("mention.json", %{user: user})] + end +end From 59dd240c0808bc895ca2b98030f5f8c2a27b9bba Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 12:10:46 +0200 Subject: [PATCH 09/26] Use token exchange method. --- lib/pleroma/web/oauth/oauth_controller.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index a6a411573..579d6b3f4 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -25,12 +25,12 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password" end end - # TODO CRITICAL - # - Check validity of auth token + # TODO + # - proper scope handling def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]), %Authorization{} = auth <- Repo.get_by(Authorization, token: params["code"], app_id: app.id), - {:ok, token} <- Token.create_token(app, Repo.get(User, auth.user_id)) do + {:ok, token} <- Token.exchange_token(app, auth) do response = %{ token_type: "Bearer", access_token: token.token, From be04f725e9398ebde446ef5664d4dbedd1eb262b Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 13:15:01 +0200 Subject: [PATCH 10/26] Add more Mastodon API methods. --- .../mastodon_api/mastodon_api_controller.ex | 39 +++++++- lib/pleroma/web/router.ex | 10 +- .../mastodon_api_controller_test.exs | 96 +++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 test/web/mastodon_api/mastodon_api_controller_test.exs diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 62522439c..3a568cf2b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,9 +1,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo} + alias Pleroma.{Repo, Activity} alias Pleroma.Web.OAuth.App alias Pleroma.Web - alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.TwitterAPI.TwitterAPI def create_app(conn, params) do with cs <- App.register_changeset(%App{}, params) |> IO.inspect, @@ -33,4 +35,37 @@ def masto_instance(conn, _params) do json(conn, response) end + + def home_timeline(%{assigns: %{user: user}} = conn, params) do + activities = ActivityPub.fetch_activities([user.ap_id | user.following], Map.put(params, "type", "Create")) + render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} + end + + def public_timeline(%{assigns: %{user: user}} = conn, params) do + params = params + |> Map.put("type", "Create") + |> Map.put("local_only", !!params["local"]) + + activities = ActivityPub.fetch_public_activities(params) + + render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} + end + + def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Repo.get(Activity, id) do + render conn, StatusView, "status.json", %{activity: activity, for: user} + end + end + + def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params) do + l = status |> String.trim |> String.length + + params = params + |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) + + if l > 0 && l < 5000 do + {:ok, activity} = TwitterAPI.create_status(user, params) + render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a8577c30b..46cbf4e4e 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -10,6 +10,7 @@ def user_fetcher(username) do pipeline :api do plug :accepts, ["json"] plug :fetch_session + plug Pleroma.Plugs.OAuthPlug plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true} end @@ -40,14 +41,21 @@ def user_fetcher(username) do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through :api - get "/instance", MastodonAPO.Controller, :masto_instance + get "/instance", MastodonAPIController, :masto_instance post "/apps", MastodonAPIController, :create_app + + get "/timelines/public", MastodonAPIController, :public_timeline + + get "/statuses/:id", MastodonAPIController, :get_status end scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through :authenticated_api get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials + get "/timelines/home", MastodonAPIController, :home_timeline + + post "/statuses", MastodonAPIController, :post_status end scope "/api", Pleroma.Web do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs new file mode 100644 index 000000000..a3692c9a0 --- /dev/null +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -0,0 +1,96 @@ +defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.{Repo, User, Activity} + alias Pleroma.Web.OStatus + + import Pleroma.Factory + + test "the home timeline", %{conn: conn} do + user = insert(:user) + following = insert(:user) + + {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) + + conn = conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + + assert length(json_response(conn, 200)) == 0 + + {:ok, user} = User.follow(user, following) + + conn = build_conn() + |> assign(:user, user) + |> get("/api/v1/timelines/home") + + assert [%{"content" => "test"}] = json_response(conn, 200) + end + + test "the public timeline", %{conn: conn} do + following = insert(:user) + + {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) + {:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") + + conn = conn + |> get("/api/v1/timelines/public") + + assert length(json_response(conn, 200)) == 2 + + conn = build_conn() + |> get("/api/v1/timelines/public", %{"local" => "True"}) + + assert [%{"content" => "test"}] = json_response(conn, 200) + end + + test "posting a status", %{conn: conn} do + user = insert(:user) + + conn = conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{"status" => "cofe"}) + + assert %{"content" => "cofe", "id" => id} = json_response(conn, 200) + assert Repo.get(Activity, id) + end + + test "replying to a status", %{conn: conn} do + user = insert(:user) + + {:ok, replied_to} = TwitterAPI.create_status(user, %{"status" => "cofe"}) + + conn = conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response(conn, 200) + + activity = Repo.get(Activity, id) + + assert activity.data["context"] == replied_to.data["context"] + assert activity.data["object"]["inReplyToStatusId"] == replied_to.id + end + + test "verify_credentials", %{conn: conn} do + user = insert(:user) + + conn = conn + |> assign(:user, user) + |> get("/api/v1/accounts/verify_credentials") + + assert %{"id" => id} = json_response(conn, 200) + assert id == user.id + end + + test "get a status", %{conn: conn} do + activity = insert(:note_activity) + + conn = conn + |> get("/api/v1/statuses/#{activity.id}") + + assert %{"id" => id} = json_response(conn, 200) + assert id == activity.id + end +end From 4dc517a0bb979793c1c2590d38efe853c68eb80c Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 13:56:51 +0200 Subject: [PATCH 11/26] Add deletion to masto api. --- lib/pleroma/web/common_api/common_api.ex | 13 +++++++++ .../mastodon_api/mastodon_api_controller.ex | 12 ++++++++ lib/pleroma/web/router.ex | 1 + .../web/twitter_api/twitter_api_controller.ex | 6 ++-- .../mastodon_api_controller_test.exs | 28 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/web/common_api/common_api.ex diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex new file mode 100644 index 000000000..a894ac9c1 --- /dev/null +++ b/lib/pleroma/web/common_api/common_api.ex @@ -0,0 +1,13 @@ +defmodule Pleroma.Web.CommonAPI do + alias Pleroma.{Repo, Activity, Object} + alias Pleroma.Web.ActivityPub.ActivityPub + + def delete(activity_id, user) do + with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), + %Object{} = object <- Object.get_by_ap_id(object_id), + true <- user.ap_id == object.data["actor"], + {:ok, delete} <- ActivityPub.delete(object) do + {:ok, delete} + end + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 3a568cf2b..af62c3df0 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -6,6 +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 def create_app(conn, params) do with cs <- App.register_changeset(%App{}, params) |> IO.inspect, @@ -68,4 +69,15 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} end end + + def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + json(conn, %{}) + else + _e -> + conn + |> put_status(403) + |> json(%{error: "Can't delete this post"}) + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 46cbf4e4e..d3cae6201 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -56,6 +56,7 @@ def user_fetcher(username) do get "/timelines/home", MastodonAPIController, :home_timeline post "/statuses", MastodonAPIController, :post_status + delete "/statuses/:id", MastodonAPIController, :delete_status end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 3ec54616a..5e0b9ea0a 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -2,6 +2,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView} alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter + alias Pleroma.Web.CommonAPI alias Pleroma.{Repo, Activity, User, Object} alias Pleroma.Web.ActivityPub.ActivityPub alias Ecto.Changeset @@ -95,10 +96,7 @@ def follow(%{assigns: %{user: user}} = conn, params) do end def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, id), - %Object{} = object <- Object.get_by_ap_id(object_id), - true <- user.ap_id == object.data["actor"], - {:ok, delete} <- ActivityPub.delete(object) |> IO.inspect do + with {:ok, delete} <- CommonAPI.delete(id, user) do json = ActivityRepresenter.to_json(delete, %{user: user, for: user}) conn |> json_reply(200, json) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index a3692c9a0..d781af675 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -93,4 +93,32 @@ test "get a status", %{conn: conn} do assert %{"id" => id} = json_response(conn, 200) assert id == activity.id end + + describe "deleting a status" do + test "when you created it", %{conn: conn} do + activity = insert(:note_activity) + author = User.get_by_ap_id(activity.data["actor"]) + + conn = conn + |> assign(:user, author) + |> delete("/api/v1/statuses/#{activity.id}") + + assert %{} = json_response(conn, 200) + + assert Repo.get(Activity, activity.id) == nil + end + + test "when you didn't create it", %{conn: conn} do + activity = insert(:note_activity) + user = insert(:user) + + conn = conn + |> assign(:user, user) + |> delete("/api/v1/statuses/#{activity.id}") + + assert %{"error" => _} = json_response(conn, 403) + + assert Repo.get(Activity, activity.id) == activity + end + end end From 66e4c710d469d7f2177c06e0dafb181d4d4abf30 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 17:48:57 +0200 Subject: [PATCH 12/26] Add reblogging to MastodonAPI. --- lib/pleroma/web/common_api/common_api.ex | 21 +++++++++++++++++++ .../mastodon_api/mastodon_api_controller.ex | 7 +++++++ .../web/mastodon_api/views/status_view.ex | 6 ++++-- lib/pleroma/web/router.ex | 2 ++ lib/pleroma/web/twitter_api/twitter_api.ex | 19 +++++++---------- .../web/twitter_api/twitter_api_controller.ex | 13 ++---------- .../mastodon_api_controller_test.exs | 14 +++++++++++++ .../twitter_api_controller_test.exs | 7 ------- test/web/twitter_api/twitter_api_test.exs | 2 +- 9 files changed, 58 insertions(+), 33 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index a894ac9c1..b1d2172c7 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -10,4 +10,25 @@ def delete(activity_id, user) do {:ok, delete} end end + + def repeat(id_or_ap_id, user) do + with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), + false <- activity.data["actor"] == user.ap_id, + object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + ActivityPub.announce(user, object) + else + _ -> + {:error, "Could not repeat"} + end + end + + # This is a hack for twidere. + def get_by_id_or_ap_id(id) do + activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) + if activity.data["type"] == "Create" do + activity + else + Activity.get_create_activity_by_object_ap_id(activity.data["object"]) + end + end end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index af62c3df0..67b5d49b3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -80,4 +80,11 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do |> json(%{error: "Can't delete this post"}) end end + + def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do + render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} + end + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 45e7d45f4..d1e5f58c5 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -7,7 +7,7 @@ def render("index.json", opts) do render_many(opts.activities, StatusView, "status.json", opts) end - def render("status.json", %{activity: %{data: %{"object" => object}} = activity}) do + def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do user = User.get_cached_by_ap_id(activity.data["actor"]) like_count = object["like_count"] || 0 @@ -21,6 +21,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} |> Enum.filter(&(&1)) |> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) + repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) + %{ id: activity.id, uri: object["id"], @@ -33,7 +35,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} created_at: object["published"], reblogs_count: announcement_count, favourites_count: like_count, - reblogged: false, + reblogged: !!repeated, favourited: false, # fix muted: false, sensitive: sensitive, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d3cae6201..4e59530ae 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -57,6 +57,8 @@ def user_fetcher(username) do post "/statuses", MastodonAPIController, :post_status delete "/statuses/:id", MastodonAPIController, :delete_status + + post "/statuses/:id/reblog", MastodonAPIController, :reblog_status end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 1ae076e24..daa53c73b 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -3,7 +3,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter alias Pleroma.Web.TwitterAPI.UserView - alias Pleroma.Web.OStatus + alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Formatter import Pleroma.Web.TwitterAPI.Utils @@ -141,17 +141,12 @@ def unfavorite(%User{} = user, %Activity{data: %{"object" => object}} = activity {:ok, status} end - def retweet(%User{} = user, %Activity{data: %{"object" => object}} = activity) do - object = Object.get_by_ap_id(object["id"]) - - {:ok, _announce_activity, object} = ActivityPub.announce(user, object) - new_data = activity.data - |> Map.put("object", object.data) - - status = %{activity | data: new_data} - |> activity_to_status(%{for: user}) - - {:ok, status} + def repeat(%User{} = user, ap_id_or_id) do + with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), + status <- activity_to_status(activity, %{for: user}) do + {:ok, status} + end end def upload(%Plug.Upload{} = file, format \\ "xml") do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 5e0b9ea0a..a07c60e06 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -167,22 +167,13 @@ def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do end def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do - activity = get_by_id_or_ap_id(id) - if activity.data["actor"] == user.ap_id do - bad_request_reply(conn, "You cannot repeat your own notice.") - else - {:ok, status} = TwitterAPI.retweet(user, activity) - response = Poison.encode!(status) - - conn - - |> json_reply(200, response) + with {:ok, status} <- TwitterAPI.repeat(user, id) do + json(conn, status) end end def register(conn, params) do with {:ok, user} <- TwitterAPI.register_user(params) do - render(conn, UserView, "show.json", %{user: user}) else {:error, errors} -> diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index d781af675..6cdb75d08 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -121,4 +121,18 @@ test "when you didn't create it", %{conn: conn} do assert Repo.get(Activity, activity.id) == activity end end + + describe "reblogging" do + test "reblogs and returns the reblogged status", %{conn: conn} do + activity = insert(:note_activity) + user = insert(:user) + + conn = conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{"id" => id, "reblogged" => true, "reblogs_count" => 1} = json_response(conn, 200) + assert activity.id == id + end + end end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 89b8c2eeb..2c89509ff 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -354,13 +354,6 @@ test "with credentials", %{conn: conn, user: current_user} do request_path = "/api/statuses/retweet/#{note_activity.id}.json" - user = Repo.get_by(User, ap_id: note_activity.data["actor"]) - response = conn - |> with_credentials(user.nickname, "test") - |> post(request_path) - assert json_response(response, 400) == %{"error" => "You cannot repeat your own notice.", - "request" => request_path} - response = conn |> with_credentials(current_user.nickname, "test") |> post(request_path) diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index bbb261eff..c1c9b2d22 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -290,7 +290,7 @@ test "it retweets a status and returns the retweet" do note_activity = insert(:note_activity) activity_user = Repo.get_by!(User, ap_id: note_activity.data["actor"]) - {:ok, status} = TwitterAPI.retweet(user, note_activity) + {:ok, status} = TwitterAPI.repeat(user, note_activity.id) updated_activity = Activity.get_by_ap_id(note_activity.data["id"]) assert status == ActivityRepresenter.to_map(updated_activity, %{user: activity_user, for: user}) From 454dc1857074c8a98b4fada6d65ed4a810f1c501 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 18:09:37 +0200 Subject: [PATCH 13/26] Add favoriting to Mastodon API. --- lib/pleroma/web/common_api/common_api.ex | 11 ++++++++++ .../mastodon_api/mastodon_api_controller.ex | 7 +++++++ .../web/mastodon_api/views/status_view.ex | 3 ++- lib/pleroma/web/router.ex | 1 + lib/pleroma/web/twitter_api/twitter_api.ex | 21 +++++++------------ .../web/twitter_api/twitter_api_controller.ex | 9 +++----- .../mastodon_api_controller_test.exs | 14 +++++++++++++ test/web/twitter_api/twitter_api_test.exs | 2 +- 8 files changed, 47 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index b1d2172c7..43cec9121 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -22,6 +22,17 @@ def repeat(id_or_ap_id, user) do end end + def favorite(id_or_ap_id, user) do + with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), + false <- activity.data["actor"] == user.ap_id, + object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + ActivityPub.like(user, object) + else + _ -> + {:error, "Could not favorite"} + end + end + # This is a hack for twidere. def get_by_id_or_ap_id(id) do activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 67b5d49b3..c0ae3fd23 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -87,4 +87,11 @@ def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} end end + + def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do + render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} + end + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d1e5f58c5..7b798506a 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -22,6 +22,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} |> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) + favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) %{ id: activity.id, @@ -36,7 +37,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} reblogs_count: announcement_count, favourites_count: like_count, reblogged: !!repeated, - favourited: false, # fix + favourited: !!favorited, muted: false, sensitive: sensitive, spoiler_text: "", diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 4e59530ae..33b51fd34 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -59,6 +59,7 @@ def user_fetcher(username) do delete "/statuses/:id", MastodonAPIController, :delete_status post "/statuses/:id/reblog", MastodonAPIController, :reblog_status + post "/statuses/:id/favourite", MastodonAPIController, :fav_status end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index daa53c73b..0c77e092c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -115,19 +115,6 @@ def unfollow(%User{} = follower, params) do end end - def favorite(%User{} = user, %Activity{data: %{"object" => object}} = activity) do - object = Object.get_by_ap_id(object["id"]) - - {:ok, _like_activity, object} = ActivityPub.like(user, object) - new_data = activity.data - |> Map.put("object", object.data) - - status = %{activity | data: new_data} - |> activity_to_status(%{for: user}) - - {:ok, status} - end - def unfavorite(%User{} = user, %Activity{data: %{"object" => object}} = activity) do object = Object.get_by_ap_id(object["id"]) @@ -149,6 +136,14 @@ def repeat(%User{} = user, ap_id_or_id) do end end + def fav(%User{} = user, ap_id_or_id) do + with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), + status <- activity_to_status(activity, %{for: user}) do + {:ok, status} + end + end + def upload(%Plug.Upload{} = file, format \\ "xml") do {:ok, object} = ActivityPub.upload(file) diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index a07c60e06..7da1291b0 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -149,12 +149,9 @@ def get_by_id_or_ap_id(id) do end def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do - activity = get_by_id_or_ap_id(id) - {:ok, status} = TwitterAPI.favorite(user, activity) - response = Poison.encode!(status) - - conn - |> json_reply(200, response) + with {:ok, status} <- TwitterAPI.fav(user, id) do + json(conn, status) + end end def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 6cdb75d08..9af49da12 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -135,4 +135,18 @@ test "reblogs and returns the reblogged status", %{conn: conn} do assert activity.id == id end end + + describe "favoriting" do + test "favs a status and returns it", %{conn: conn} do + activity = insert(:note_activity) + user = insert(:user) + + conn = conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/favourite") + + assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = json_response(conn, 200) + assert activity.id == id + end + end end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index c1c9b2d22..a9494f424 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -264,7 +264,7 @@ test "it favorites a status, returns the updated status" do note_activity = insert(:note_activity) activity_user = Repo.get_by!(User, ap_id: note_activity.data["actor"]) - {:ok, status} = TwitterAPI.favorite(user, note_activity) + {:ok, status} = TwitterAPI.fav(user, note_activity.id) updated_activity = Activity.get_by_ap_id(note_activity.data["id"]) assert status == ActivityRepresenter.to_map(updated_activity, %{user: activity_user, for: user}) From d625d8db7d6041e85ef7c7c1a8b617c9bba36a98 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 18:30:02 +0200 Subject: [PATCH 14/26] Add unfav to Mastodon API. --- lib/pleroma/web/common_api/common_api.ex | 11 ++++++++++ .../mastodon_api/mastodon_api_controller.ex | 9 +++++++- lib/pleroma/web/router.ex | 1 + lib/pleroma/web/twitter_api/twitter_api.ex | 21 +++++++------------ .../web/twitter_api/twitter_api_controller.ex | 9 +++----- .../mastodon_api_controller_test.exs | 18 +++++++++++++++- test/web/twitter_api/twitter_api_test.exs | 2 +- 7 files changed, 49 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 43cec9121..b08138534 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -33,6 +33,17 @@ def favorite(id_or_ap_id, user) do end end + def unfavorite(id_or_ap_id, user) do + with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), + false <- activity.data["actor"] == user.ap_id, + object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + ActivityPub.unlike(user, object) + else + _ -> + {:error, "Could not unfavorite"} + end + end + # This is a hack for twidere. def get_by_id_or_ap_id(id) do activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index c0ae3fd23..84b94b352 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -89,7 +89,14 @@ def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do end def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do - with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user), + with {:ok, _fav, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do + render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} + end + end + + def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do + with {:ok, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user), %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 33b51fd34..33c3aa53d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -60,6 +60,7 @@ def user_fetcher(username) do post "/statuses/:id/reblog", MastodonAPIController, :reblog_status post "/statuses/:id/favourite", MastodonAPIController, :fav_status + post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status end scope "/api", Pleroma.Web do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 0c77e092c..657823d1d 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -115,19 +115,6 @@ def unfollow(%User{} = follower, params) do end end - def unfavorite(%User{} = user, %Activity{data: %{"object" => object}} = activity) do - object = Object.get_by_ap_id(object["id"]) - - {:ok, object} = ActivityPub.unlike(user, object) - new_data = activity.data - |> Map.put("object", object.data) - - status = %{activity | data: new_data} - |> activity_to_status(%{for: user}) - - {:ok, status} - end - def repeat(%User{} = user, ap_id_or_id) do with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user), %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), @@ -144,6 +131,14 @@ def fav(%User{} = user, ap_id_or_id) do end end + def unfav(%User{} = user, ap_id_or_id) do + with {:ok, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user), + %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), + status <- activity_to_status(activity, %{for: user}) do + {:ok, status} + end + end + def upload(%Plug.Upload{} = file, format \\ "xml") do {:ok, object} = ActivityPub.upload(file) diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 7da1291b0..62a2b4f50 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -155,12 +155,9 @@ def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do end def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do - activity = get_by_id_or_ap_id(id) - {:ok, status} = TwitterAPI.unfavorite(user, activity) - response = Poison.encode!(status) - - conn - |> json_reply(200, response) + with {:ok, status} <- TwitterAPI.unfav(user, id) do + json(conn, status) + end end def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 9af49da12..dc925e2c8 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3,7 +3,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.{Repo, User, Activity} - alias Pleroma.Web.OStatus + alias Pleroma.Web.{OStatus, CommonAPI} import Pleroma.Factory @@ -149,4 +149,20 @@ test "favs a status and returns it", %{conn: conn} do assert activity.id == id end end + + describe "unfavoriting" do + test "unfavorites a status and returns it", %{conn: conn} do + activity = insert(:note_activity) + user = insert(:user) + + {:ok, _, _} = CommonAPI.favorite(activity.id, user) + + conn = conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + + assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = json_response(conn, 200) + assert activity.id == id + end + end end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index a9494f424..d5c94d2c7 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -280,7 +280,7 @@ test "it unfavorites a status, returns the updated status" do updated_activity = Activity.get_by_ap_id(note_activity.data["id"]) assert ActivityRepresenter.to_map(updated_activity, %{user: activity_user, for: user})["fave_num"] == 1 - {:ok, status} = TwitterAPI.unfavorite(user, note_activity) + {:ok, status} = TwitterAPI.unfav(user, note_activity.id) assert status["fave_num"] == 0 end From 5fe9e4dd3feaeea9e35c3ef126e8c3b0ee8601a6 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 19:03:57 +0200 Subject: [PATCH 15/26] Do oauth redirect. --- lib/pleroma/web/oauth/oauth_controller.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 579d6b3f4..4672ce00e 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -14,14 +14,19 @@ def authorize(conn, params) do } end - def create_authorization(conn, %{"authorization" => %{"name" => name, "password" => password, "client_id" => client_id}} = params) do + def create_authorization(conn, %{"authorization" => %{"name" => name, "password" => password, "client_id" => client_id, "redirect_uri" => redirect_uri}} = params) do with %User{} = user <- User.get_cached_by_nickname(name), true <- Pbkdf2.checkpw(password, user.password_hash), %App{} = app <- Repo.get_by(App, client_id: client_id), {:ok, auth} <- Authorization.create_authorization(app, user) do - render conn, "results.html", %{ - auth: auth - } + if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do + render conn, "results.html", %{ + auth: auth + } + else + url = "#{redirect_uri}?code=#{auth.token}" + redirect(conn, external: url) + end end end From d66d69c3b429b8ad18d4247fe6abd0ee9e1a8ece Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sat, 9 Sep 2017 19:19:13 +0200 Subject: [PATCH 16/26] Small hack to make notifications return empty for now. --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 6 ++++++ lib/pleroma/web/router.ex | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 84b94b352..c81d58d64 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.CommonAPI + import Logger def create_app(conn, params) do with cs <- App.register_changeset(%App{}, params) |> IO.inspect, @@ -101,4 +102,9 @@ def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} end end + + def empty_array(conn, _) do + Logger.debug("Unimplemented, returning an empty array") + json(conn, []) + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 33c3aa53d..84bf6791d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -61,6 +61,8 @@ def user_fetcher(username) do post "/statuses/:id/reblog", MastodonAPIController, :reblog_status post "/statuses/:id/favourite", MastodonAPIController, :fav_status post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status + + get "/notifications", MastodonAPIController, :empty_array end scope "/api", Pleroma.Web do From e8975d06bed653f362777ee7046f8bb0129e461e Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 10:37:34 +0200 Subject: [PATCH 17/26] Add header image to masto api. --- lib/pleroma/web/mastodon_api/views/account_view.ex | 9 +++++++-- test/web/mastodon_api/account_view_test.exs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 5f6ca84d0..35a130b1e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -2,10 +2,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do use Pleroma.Web, :view alias Pleroma.User + defp image_url(%{"url" => [ %{ "href" => href } | t ]}), do: href + defp image_url(_), do: nil + def render("account.json", %{user: user}) do image = User.avatar_url(user) user_info = User.user_info(user) + header = image_url(user.info["banner"]) || "https://placehold.it/700x335" + %{ id: user.id, username: user.nickname, @@ -20,8 +25,8 @@ def render("account.json", %{user: user}) do url: user.ap_id, avatar: image, avatar_static: image, - header: "", - header_static: "" + header: header, + header_static: header } end diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index f0c8673ad..59fac6d95 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -20,8 +20,8 @@ test "Represent a user account" do url: user.ap_id, avatar: "https://placehold.it/48x48", avatar_static: "https://placehold.it/48x48", - header: "", - header_static: "" + header: "https://placehold.it/700x335", + header_static: "https://placehold.it/700x335" } assert expected == AccountView.render("account.json", %{user: user}) From 96473dfac02d901e5b915ca56a34ce67b30c10d5 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 10:49:15 +0200 Subject: [PATCH 18/26] Reverse mastodon timeline data. --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index c81d58d64..4401a37a3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -40,6 +40,7 @@ def masto_instance(conn, _params) do def home_timeline(%{assigns: %{user: user}} = conn, params) do activities = ActivityPub.fetch_activities([user.ap_id | user.following], Map.put(params, "type", "Create")) + |> Enum.reverse render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} end @@ -49,6 +50,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do |> Map.put("local_only", !!params["local"]) activities = ActivityPub.fetch_public_activities(params) + |> Enum.reverse render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} end From fc10875895abd9add5a7834c4b5a64cc5b9401f8 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 11:51:01 +0200 Subject: [PATCH 19/26] Add attachments to mastoapi statuses. --- .../web/mastodon_api/views/status_view.ex | 22 +++++++++++++++- test/web/mastodon_api/status_view_test.exs | 26 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7b798506a..686ffd29d 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -24,6 +24,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) + attachments = render_many(object["attachment"] || [], StatusView, "attachment.json", as: :attachment) + %{ id: activity.id, uri: object["id"], @@ -42,11 +44,29 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity} sensitive: sensitive, spoiler_text: "", visibility: "public", - media_attachments: [], # fix + media_attachments: attachments, mentions: mentions, tags: [], # fix, application: nil, language: nil } end + + def render("attachment.json", %{attachment: attachment}) do + [%{"mediaType" => media_type, "href" => href} | _] = attachment["url"] + + type = cond do + String.contains?(media_type, "image") -> "image" + String.contains?(media_type, "video") -> "video" + true -> "unknown" + end + + %{ + id: attachment["uuid"], + url: href, + remote_url: href, + preview_url: href, + type: type + } + end end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index b8a96f71a..a12fc8244 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do use Pleroma.DataCase alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} - alias Pleroma.User + alias Pleroma.{User, Object} alias Pleroma.Web.OStatus import Pleroma.Factory @@ -50,4 +50,28 @@ test "contains mentions" do assert status.mentions == [AccountView.render("mention.json", %{user: user})] end + + test "attachments" do + incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") + object = %{ + "type" => "Image", + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "someurl" + } + ], + "uuid" => 6 + } + + expected = %{ + id: 6, + type: "image", + url: "someurl", + remote_url: "someurl", + preview_url: "someurl" + } + + assert expected == StatusView.render("attachment.json", %{attachment: object}) + end end From 8672d4d12b7be3689386567ee93869292275439c Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 15:00:13 +0200 Subject: [PATCH 20/26] Add context to mastodonAPI. --- .../web/mastodon_api/mastodon_api_controller.ex | 13 +++++++++++++ lib/pleroma/web/router.ex | 1 + 2 files changed, 14 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 4401a37a3..900f9e3da 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -61,6 +61,19 @@ def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Activity{} = activity <- Repo.get(Activity, id), + activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"]), + %{true: ancestors, false: descendants} <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do + result = %{ + ancestors: StatusView.render("index.json", for: user, activities: ancestors, as: :activity) |> Enum.reverse, + descendants: StatusView.render("index.json", for: user, activities: descendants, as: :activity) |> Enum.reverse, + } + + json(conn, result) + end + end + def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params) do l = status |> String.trim |> String.length diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 84bf6791d..5246b3c41 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -47,6 +47,7 @@ def user_fetcher(username) do get "/timelines/public", MastodonAPIController, :public_timeline get "/statuses/:id", MastodonAPIController, :get_status + get "/statuses/:id/context", MastodonAPIController, :get_context end scope "/api/v1", Pleroma.Web.MastodonAPI do From b8912ff954a0aa6426eb2205da82db8bee6c5a6a Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 17:20:53 +0200 Subject: [PATCH 21/26] Fix masto api context. --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 900f9e3da..1aa7f43ab 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -64,10 +64,11 @@ def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Repo.get(Activity, id), activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"]), - %{true: ancestors, false: descendants} <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do + activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), + grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do result = %{ - ancestors: StatusView.render("index.json", for: user, activities: ancestors, as: :activity) |> Enum.reverse, - descendants: StatusView.render("index.json", for: user, activities: descendants, as: :activity) |> Enum.reverse, + ancestors: StatusView.render("index.json", for: user, activities: grouped_activities[true] || [], as: :activity) |> Enum.reverse, + descendants: StatusView.render("index.json", for: user, activities: grouped_activities[false] || [], as: :activity) |> Enum.reverse, } json(conn, result) From 7616b202ea6ab9cd2db107eea59aba1393f4f996 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Sun, 10 Sep 2017 17:46:43 +0200 Subject: [PATCH 22/26] Add user timelines to Masto Api. --- .../web/mastodon_api/mastodon_api_controller.ex | 15 ++++++++++++++- lib/pleroma/web/router.ex | 2 ++ .../mastodon_api_controller_test.exs | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 1aa7f43ab..16ee434c6 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo, Activity} + alias Pleroma.{Repo, Activity, User} alias Pleroma.Web.OAuth.App alias Pleroma.Web alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} @@ -55,6 +55,19 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} end + def user_statuses(%{assigns: %{user: user}} = conn, params) do + with %User{ap_id: ap_id} <- Repo.get(User, params["id"]) do + params = params + |> Map.put("type", "Create") + |> Map.put("actor_id", ap_id) + + activities = ActivityPub.fetch_activities([], params) + |> Enum.reverse + + render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity} + end + end + def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Repo.get(Activity, id) do render conn, StatusView, "status.json", %{activity: activity, for: user} diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5246b3c41..9e725641d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -48,6 +48,8 @@ def user_fetcher(username) do get "/statuses/:id", MastodonAPIController, :get_status get "/statuses/:id/context", MastodonAPIController, :get_context + + get "/accounts/:id/statuses", MastodonAPIController, :user_statuses end scope "/api/v1", Pleroma.Web.MastodonAPI do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index dc925e2c8..e87430d3f 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -165,4 +165,20 @@ test "unfavorites a status and returns it", %{conn: conn} do assert activity.id == id end end + + describe "user timelines" do + test "gets a users statuses", %{conn: conn} do + _note = insert(:note_activity) + note_two = insert(:note_activity) + + user = User.get_by_ap_id(note_two.data["actor"]) + + conn = conn + |> get("/api/v1/accounts/#{user.id}/statuses") + + assert [%{"id" => id}] = json_response(conn, 200) + + assert id == note_two.id + end + end end From 61adf676d56db274cb4688a137787e8806e77be9 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Mon, 11 Sep 2017 16:15:28 +0200 Subject: [PATCH 23/26] Add basic mastodon notification support. --- lib/pleroma/activity.ex | 3 +- lib/pleroma/notification.ex | 38 +++++++++++++++++++ lib/pleroma/user.ex | 11 +++++- lib/pleroma/web/activity_pub/activity_pub.ex | 6 ++- .../mastodon_api/mastodon_api_controller.ex | 16 +++++++- lib/pleroma/web/router.ex | 2 +- .../20170911123607_create_notifications.exs | 15 ++++++++ test/notification_test.exs | 23 +++++++++++ 8 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 lib/pleroma/notification.ex create mode 100644 priv/repo/migrations/20170911123607_create_notifications.exs create mode 100644 test/notification_test.exs diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index f226c4c5f..9a5e6fc78 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -1,11 +1,12 @@ defmodule Pleroma.Activity do use Ecto.Schema - alias Pleroma.{Repo, Activity} + alias Pleroma.{Repo, Activity, Notification} import Ecto.Query schema "activities" do field :data, :map field :local, :boolean, default: true + has_many :notifications, Notification timestamps() end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex new file mode 100644 index 000000000..f8835fce6 --- /dev/null +++ b/lib/pleroma/notification.ex @@ -0,0 +1,38 @@ +defmodule Pleroma.Notification do + use Ecto.Schema + alias Pleroma.{User, Activity, Notification, Repo} + import Ecto.Query + + schema "notifications" do + field :seen, :boolean, default: false + belongs_to :user, Pleroma.User + belongs_to :activity, Pleroma.Activity + + timestamps() + end + + def for_user(user, opts \\ %{}) do + query = from n in Notification, + where: n.user_id == ^user.id, + order_by: [desc: n.id], + preload: [:activity], + limit: 20 + Repo.all(query) + end + + def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create"] do + users = User.get_notified_from_activity(activity) + + notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) + {:ok, notifications} + end + def create_notifications(_), do: {:ok, []} + + # TODO move to sql, too. + def create_notification(%Activity{} = activity, %User{} = user) do + notification = %Notification{user_id: user.id, activity_id: activity.id} + {:ok, notification} = Repo.insert(notification) + notification + end +end + diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4f5fcab5b..39d8cca76 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2,7 +2,7 @@ defmodule Pleroma.User do use Ecto.Schema import Ecto.{Changeset, Query} - alias Pleroma.{Repo, User, Object, Web} + alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Comeonin.Pbkdf2 alias Pleroma.Web.{OStatus, Websub} alias Pleroma.Web.ActivityPub.ActivityPub @@ -22,6 +22,7 @@ defmodule Pleroma.User do field :local, :boolean, default: true field :info, :map, default: %{} field :follower_address, :string + has_many :notifications, Notification timestamps() end @@ -239,4 +240,12 @@ def update_follower_count(%User{} = user) do Repo.update(cs) end + + def get_notified_from_activity(%Activity{data: %{"to" => to}} = activity) do + query = from u in User, + where: u.ap_id in ^to, + where: u.local == true + + Repo.all(query) + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8ae321658..e3dce9cba 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1,5 +1,5 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do - alias Pleroma.{Activity, Repo, Object, Upload, User, Web} + alias Pleroma.{Activity, Repo, Object, Upload, User, Web, Notification} alias Ecto.{Changeset, UUID} import Ecto.Query import Pleroma.Web.ActivityPub.Utils @@ -9,7 +9,9 @@ def insert(map, local \\ true) when is_map(map) do with nil <- Activity.get_by_ap_id(map["id"]), map <- lazy_put_activity_defaults(map), :ok <- insert_full_object(map) do - Repo.insert(%Activity{data: map, local: local}) + {:ok, activity} = Repo.insert(%Activity{data: map, local: local}) + Notification.create_notifications(activity) + {:ok, activity} else %Activity{} = activity -> {:ok, activity} error -> {:error, error} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 16ee434c6..07272e5b3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo, Activity, User} + alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.Web.OAuth.App alias Pleroma.Web alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} @@ -132,6 +132,20 @@ def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do end end + def notifications(%{assigns: %{user: user}} = conn, params) do + notifications = Notification.for_user(user, params) + result = Enum.map(notifications, fn (%{id: id, activity: activity, inserted_at: created_at}) -> + actor = User.get_cached_by_ap_id(activity.data["actor"]) + case activity.data["type"] do + "Create" -> %{ id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity})} + _ -> nil + end + end) + |> Enum.filter(&(&1)) + + json(conn, result) + end + def empty_array(conn, _) do Logger.debug("Unimplemented, returning an empty array") json(conn, []) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9e725641d..161635558 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -65,7 +65,7 @@ def user_fetcher(username) do post "/statuses/:id/favourite", MastodonAPIController, :fav_status post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status - get "/notifications", MastodonAPIController, :empty_array + get "/notifications", MastodonAPIController, :notifications end scope "/api", Pleroma.Web do diff --git a/priv/repo/migrations/20170911123607_create_notifications.exs b/priv/repo/migrations/20170911123607_create_notifications.exs new file mode 100644 index 000000000..5be809fb8 --- /dev/null +++ b/priv/repo/migrations/20170911123607_create_notifications.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateNotifications do + use Ecto.Migration + + def change do + create table(:notifications) do + add :user_id, references(:users, on_delete: :delete_all) + add :activity_id, references(:activities, on_delete: :delete_all) + add :seen, :boolean, default: false + + timestamps() + end + + create index(:notifications, [:user_id]) + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs new file mode 100644 index 000000000..f50b3cb24 --- /dev/null +++ b/test/notification_test.exs @@ -0,0 +1,23 @@ +defmodule Pleroma.NotificationTest do + use Pleroma.DataCase + alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.{User, Notification} + import Pleroma.Factory + + describe "create_notifications" do + test "notifies someone when they are directly addressed" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"}) + + {:ok, [notification, other_notification]} = Notification.create_notifications(activity) + + assert notification.user_id == other_user.id + assert notification.activity_id == activity.id + assert other_notification.user_id == third_user.id + assert other_notification.activity_id == activity.id + end + end +end From bcce3e5dd2c9ba262d73d398f3e8a14eee21f009 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Mon, 11 Sep 2017 20:41:05 +0200 Subject: [PATCH 24/26] Add favorites to notifications. --- lib/pleroma/notification.ex | 2 +- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index f8835fce6..031f71091 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -20,7 +20,7 @@ def for_user(user, opts \\ %{}) do Repo.all(query) end - def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create"] do + def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like"] do users = User.get_notified_from_activity(activity) notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 07272e5b3..3804a39f0 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -137,7 +137,11 @@ def notifications(%{assigns: %{user: user}} = conn, params) do result = Enum.map(notifications, fn (%{id: id, activity: activity, inserted_at: created_at}) -> actor = User.get_cached_by_ap_id(activity.data["actor"]) case activity.data["type"] do - "Create" -> %{ id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity})} + "Create" -> + %{id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity})} + "Like" -> + liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) + %{id: id, type: "favourite", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: liked_activity})} _ -> nil end end) From 3bad294058b630a4542adc869f9646ba3364fd7a Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Mon, 11 Sep 2017 20:43:25 +0200 Subject: [PATCH 25/26] Add reblogs to notifications. --- lib/pleroma/notification.ex | 2 +- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 031f71091..8cd09ad8e 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -20,7 +20,7 @@ def for_user(user, opts \\ %{}) do Repo.all(query) end - def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like"] do + def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like", "Announce"] do users = User.get_notified_from_activity(activity) notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 3804a39f0..811162196 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -142,6 +142,9 @@ def notifications(%{assigns: %{user: user}} = conn, params) do "Like" -> liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) %{id: id, type: "favourite", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: liked_activity})} + "Announce" -> + announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) + %{id: id, type: "reblog", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: announced_activity})} _ -> nil end end) From 464c33e9a1daf0477be050209043d2c26943d1fd Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Mon, 11 Sep 2017 20:53:11 +0200 Subject: [PATCH 26/26] Add follow notifications. --- lib/pleroma/notification.ex | 2 +- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 8cd09ad8e..4a9e835bf 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -20,7 +20,7 @@ def for_user(user, opts \\ %{}) do Repo.all(query) end - def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like", "Announce"] do + def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do users = User.get_notified_from_activity(activity) notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 811162196..9e4d13b3a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -145,6 +145,8 @@ def notifications(%{assigns: %{user: user}} = conn, params) do "Announce" -> announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) %{id: id, type: "reblog", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: announced_activity})} + "Follow" -> + %{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})} _ -> nil end end)