Merge branch 'develop' into oauth2
authorRoger Braun <roger@rogerbraun.net>
Mon, 11 Sep 2017 18:54:44 +0000 (20:54 +0200)
committerRoger Braun <roger@rogerbraun.net>
Mon, 11 Sep 2017 18:54:44 +0000 (20:54 +0200)
37 files changed:
lib/pleroma/activity.ex
lib/pleroma/notification.ex [new file with mode: 0644]
lib/pleroma/plugs/oauth_plug.ex [new file with mode: 0644]
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/common_api/common_api.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/mastodon_api.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/views/account_view.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/views/status_view.ex [new file with mode: 0644]
lib/pleroma/web/oauth/app.ex [new file with mode: 0644]
lib/pleroma/web/oauth/authorization.ex [new file with mode: 0644]
lib/pleroma/web/oauth/oauth_controller.ex [new file with mode: 0644]
lib/pleroma/web/oauth/oauth_view.ex [new file with mode: 0644]
lib/pleroma/web/oauth/token.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/templates/layout/app.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/o_auth/o_auth/results.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex [new file with mode: 0644]
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
lib/pleroma/web/views/layout_view.ex [new file with mode: 0644]
mix.exs
mix.lock
priv/repo/migrations/20170906120646_add_mastodon_apps.exs [new file with mode: 0644]
priv/repo/migrations/20170906143140_create_o_auth_authorizations.exs [new file with mode: 0644]
priv/repo/migrations/20170906152508_create_o_auth_token.exs [new file with mode: 0644]
priv/repo/migrations/20170911123607_create_notifications.exs [new file with mode: 0644]
test/notification_test.exs [new file with mode: 0644]
test/web/mastodon_api/account_view_test.exs [new file with mode: 0644]
test/web/mastodon_api/mastodon_api_controller_test.exs [new file with mode: 0644]
test/web/mastodon_api/status_view_test.exs [new file with mode: 0644]
test/web/oauth/authorization_test.exs [new file with mode: 0644]
test/web/oauth/token_test.exs [new file with mode: 0644]
test/web/twitter_api/twitter_api_controller_test.exs
test/web/twitter_api/twitter_api_test.exs

index f226c4c5f351d797a9eac90bc835645c230c10bd..9a5e6fc787c5035dcb5d5e5a7a1f5af058fbdfed 100644 (file)
@@ -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 (file)
index 0000000..4a9e835
--- /dev/null
@@ -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", "Like", "Announce", "Follow"] 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/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
new file mode 100644 (file)
index 0000000..fc2a907
--- /dev/null
@@ -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
index 4f5fcab5b549dd58cfdefec2e5a2b9723ec23d93..39d8cca768aeb7222b654de3d119fbf2ba409683 100644 (file)
@@ -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 @@ defmodule Pleroma.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
index db1302738d7276176d297fcae3a9154df5ea9c99..e3dce9cba03a6da4d15193d94e286fc0ff200995 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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}
@@ -133,6 +135,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 +152,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_local(opts)
     |> restrict_max(opts)
     |> restrict_actor(opts)
+    |> restrict_type(opts)
     |> Repo.all
     |> Enum.reverse
   end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
new file mode 100644 (file)
index 0000000..b081385
--- /dev/null
@@ -0,0 +1,56 @@
+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
+
+  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
+
+  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
+
+  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)
+    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.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
new file mode 100644 (file)
index 0000000..e69de29
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 (file)
index 0000000..9e4d13b
--- /dev/null
@@ -0,0 +1,162 @@
+defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
+  use Pleroma.Web, :controller
+  alias Pleroma.{Repo, Activity, User, Notification}
+  alias Pleroma.Web.OAuth.App
+  alias Pleroma.Web
+  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView}
+  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,
+         {: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 = 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
+
+  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
+
+  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)
+    |> Enum.reverse
+
+    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}
+    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"]),
+         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: 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)
+    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
+
+  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
+
+  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
+
+  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+    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
+  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})}
+        "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})}
+        "Follow" ->
+          %{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})}
+        _ -> nil
+      end
+    end)
+    |> Enum.filter(&(&1))
+
+    json(conn, result)
+  end
+
+  def empty_array(conn, _) do
+    Logger.debug("Unimplemented, returning an empty array")
+    json(conn, [])
+  end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
new file mode 100644 (file)
index 0000000..35a130b
--- /dev/null
@@ -0,0 +1,41 @@
+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,
+      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,
+      header_static: header
+    }
+  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/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
new file mode 100644 (file)
index 0000000..686ffd2
--- /dev/null
@@ -0,0 +1,72 @@
+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} = opts) 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)
+
+    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"],
+      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: !!repeated,
+      favourited: !!favorited,
+      muted: false,
+      sensitive: sensitive,
+      spoiler_text: "",
+      visibility: "public",
+      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/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
new file mode 100644 (file)
index 0000000..ff52ba8
--- /dev/null
@@ -0,0 +1,29 @@
+defmodule Pleroma.Web.OAuth.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/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
new file mode 100644 (file)
index 0000000..1ba5be6
--- /dev/null
@@ -0,0 +1,47 @@
+defmodule Pleroma.Web.OAuth.Authorization do
+  use Ecto.Schema
+
+  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
+    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
+
+  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/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
new file mode 100644 (file)
index 0000000..4672ce0
--- /dev/null
@@ -0,0 +1,49 @@
+defmodule Pleroma.Web.OAuth.OAuthController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Web.OAuth.{Authorization, Token, App}
+  alias Pleroma.{Repo, User}
+  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, "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
+      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
+
+  # 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.exchange_token(app, auth) 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 (file)
index 0000000..b3923fc
--- /dev/null
@@ -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 (file)
index 0000000..828a966
--- /dev/null
@@ -0,0 +1,38 @@
+defmodule Pleroma.Web.OAuth.Token do
+  use Ecto.Schema
+
+  alias Pleroma.{User, Repo}
+  alias Pleroma.Web.OAuth.{Token, App, Authorization}
+
+  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 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
+
+    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
index c20ec3e801904e656693a26e70def54e35c52837..161635558b6ddcec1b1040d62fec44903b95826c 100644 (file)
@@ -10,12 +10,14 @@ defmodule Pleroma.Web.Router 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
 
   pipeline :authenticated_api do
     plug :accepts, ["json"]
     plug :fetch_session
+    plug Pleroma.Plugs.OAuthPlug
     plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
   end
 
@@ -27,14 +29,43 @@ defmodule Pleroma.Web.Router do
     plug :accepts, ["json", "xml"]
   end
 
-  pipeline :masto_config do
-    plug :accepts, ["json"]
+  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.MastodonAPI do
+    pipe_through :api
+    get "/instance", MastodonAPIController, :masto_instance
+    post "/apps", MastodonAPIController, :create_app
+
+    get "/timelines/public", MastodonAPIController, :public_timeline
+
+    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 do
-    pipe_through :masto_config
-    # TODO: Move this
-    get "/instance", TwitterAPI.UtilController, :masto_instance
+  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
+    delete "/statuses/:id", MastodonAPIController, :delete_status
+
+    post "/statuses/:id/reblog", MastodonAPIController, :reblog_status
+    post "/statuses/:id/favourite", MastodonAPIController, :fav_status
+    post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status
+
+    get "/notifications", MastodonAPIController, :notifications
   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 (file)
index 0000000..6cc3b7a
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset=utf-8 />
+    <title>Pleroma</title>
+  </head>
+  <body>
+    <h1>Welcome to Pleroma</h1>
+    <%= render @view_module, @view_template, assigns %>
+  </body>
+</html>
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 (file)
index 0000000..8443d90
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>Successfully authorized</h1>
+<h2>Token code is <%= @auth.token %></h2>
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 (file)
index 0000000..ce295ed
--- /dev/null
@@ -0,0 +1,14 @@
+<h2>OAuth Authorization</h2>
+<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
+<%= label f, :name, "Name" %>
+<%= text_input f, :name %>
+<br>
+<%= label f, :password, "Password" %>
+<%= password_input f, :password %>
+<br>
+<%= 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 %>
index 285b4d105d8db7cbf09bdf9143e05087cfce88ca..41881e742c98e0fd6bc4174ea784f1b6b9eb246e 100644 (file)
@@ -42,16 +42,4 @@ defmodule Pleroma.Web.TwitterAPI.UtilController 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
index 1ae076e2430eb5628e08ee9aeb1a329291785cb9..657823d1db281a21254165e76146afc3bb70403a 100644 (file)
@@ -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
@@ -115,43 +115,28 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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}
+  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 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}
+  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 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 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
index 3ec54616a7995597de866b6e387f0b9cb98f3058..62a2b4f500787fbb56b3b119546a0ec18ec39026 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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)
@@ -151,40 +149,25 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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
-    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
-    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/lib/pleroma/web/views/layout_view.ex b/lib/pleroma/web/views/layout_view.ex
new file mode 100644 (file)
index 0000000..d4d4c3b
--- /dev/null
@@ -0,0 +1,3 @@
+defmodule Pleroma.Web.LayoutView do
+  use Pleroma.Web, :view
+end
diff --git a/mix.exs b/mix.exs
index f5457ee080177dbfc0897bbea4739edcbf88f946..00733c26a64fe3a085b6f873159a109bd61f1af5 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -28,7 +28,7 @@ defmodule Pleroma.Mixfile do
   #
   # 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,12 +37,12 @@ defmodule Pleroma.Mixfile 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"},
      {: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.
index e43463aeff306668c9189f96e29d031b67109710..ac9ec5384a2029a5fb5da38bba934e92d7017c84 100644 (file)
--- a/mix.lock
+++ b/mix.lock
   "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]}]},
diff --git a/priv/repo/migrations/20170906120646_add_mastodon_apps.exs b/priv/repo/migrations/20170906120646_add_mastodon_apps.exs
new file mode 100644 (file)
index 0000000..d3dd317
--- /dev/null
@@ -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 (file)
index 0000000..b433287
--- /dev/null
@@ -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 (file)
index 0000000..7f8550f
--- /dev/null
@@ -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
diff --git a/priv/repo/migrations/20170911123607_create_notifications.exs b/priv/repo/migrations/20170911123607_create_notifications.exs
new file mode 100644 (file)
index 0000000..5be809f
--- /dev/null
@@ -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 (file)
index 0000000..f50b3cb
--- /dev/null
@@ -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
diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs
new file mode 100644 (file)
index 0000000..59fac6d
--- /dev/null
@@ -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: "https://placehold.it/700x335",
+      header_static: "https://placehold.it/700x335"
+    }
+
+    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
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 (file)
index 0000000..e87430d
--- /dev/null
@@ -0,0 +1,184 @@
+defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Web.TwitterAPI.TwitterAPI
+  alias Pleroma.{Repo, User, Activity}
+  alias Pleroma.Web.{OStatus, CommonAPI}
+
+  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
+
+  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
+
+  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
+
+  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
+
+  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
+
+  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
diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs
new file mode 100644 (file)
index 0000000..a12fc82
--- /dev/null
@@ -0,0 +1,77 @@
+defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView}
+  alias Pleroma.{User, Object}
+  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
+
+  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
diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs
new file mode 100644 (file)
index 0000000..52441fa
--- /dev/null
@@ -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 (file)
index 0000000..3bd7639
--- /dev/null
@@ -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
index 89b8c2eeb8996d87e02d9014f5ffaa922c486ad3..2c89509ff1bed8aad114039ab4f01ed31cdf4d84 100644 (file)
@@ -354,13 +354,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest 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)
index bbb261eff3c143459a90b9058b3ba4827fadd3cc..d5c94d2c7d1f5a8a58e4cb7f3b60b0be37b34759 100644 (file)
@@ -264,7 +264,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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})
@@ -280,7 +280,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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
@@ -290,7 +290,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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})