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..4a9e835bf
--- /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", "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
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/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 db1302738..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}
@@ -133,6 +135,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 +152,7 @@ def fetch_activities(recipients, opts \\ %{}) 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
index 000000000..b08138534
--- /dev/null
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -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
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..9e4d13b3a
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -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
index 000000000..35a130b1e
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -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
index 000000000..686ffd29d
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -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
index 000000000..ff52ba82e
--- /dev/null
+++ b/lib/pleroma/web/oauth/app.ex
@@ -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
index 000000000..1ba5be602
--- /dev/null
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -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
index 000000000..4672ce00e
--- /dev/null
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -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
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..828a966fb
--- /dev/null
+++ b/lib/pleroma/web/oauth/token.ex
@@ -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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index c20ec3e80..161635558 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -10,12 +10,14 @@ 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
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 @@ def user_fetcher(username) do
plug :accepts, ["json", "xml"]
end
- pipeline :masto_config do
- plug :accepts, ["json"]
+ pipeline :oauth do
+ plug :accepts, ["html", "json"]
end
- scope "/api/v1", Pleroma.Web do
- pipe_through :masto_config
- # TODO: Move this
- get "/instance", TwitterAPI.UtilController, :masto_instance
+ 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.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
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/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
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 1ae076e24..657823d1d 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
@@ -115,43 +115,28 @@ 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}
+ 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
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 3ec54616a..62a2b4f50 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)
@@ -151,40 +149,25 @@ 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
- 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
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/mix.exs b/mix.exs
index f5457ee08..00733c26a 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,12 +37,12 @@ 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"},
{: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.
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]}]},
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
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
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..59fac6d95
--- /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: "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
index 000000000..e87430d3f
--- /dev/null
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -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
index 000000000..a12fc8244
--- /dev/null
+++ b/test/web/mastodon_api/status_view_test.exs
@@ -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
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
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..d5c94d2c7 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})
@@ -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
@@ -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})