forked from AkkomaGang/akkoma
Merge branch 'oauth2' into 'develop'
Mastodon API See merge request !27
This commit is contained in:
commit
2b21c05105
37 changed files with 1140 additions and 99 deletions
|
@ -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
|
||||
|
|
38
lib/pleroma/notification.ex
Normal file
38
lib/pleroma/notification.ex
Normal file
|
@ -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
|
||||
|
22
lib/pleroma/plugs/oauth_plug.ex
Normal file
22
lib/pleroma/plugs/oauth_plug.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
56
lib/pleroma/web/common_api/common_api.ex
Normal file
56
lib/pleroma/web/common_api/common_api.ex
Normal file
|
@ -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
|
0
lib/pleroma/web/mastodon_api/mastodon_api.ex
Normal file
0
lib/pleroma/web/mastodon_api/mastodon_api.ex
Normal file
162
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
Normal file
162
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
Normal file
|
@ -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
|
41
lib/pleroma/web/mastodon_api/views/account_view.ex
Normal file
41
lib/pleroma/web/mastodon_api/views/account_view.ex
Normal file
|
@ -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
|
72
lib/pleroma/web/mastodon_api/views/status_view.ex
Normal file
72
lib/pleroma/web/mastodon_api/views/status_view.ex
Normal file
|
@ -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
|
29
lib/pleroma/web/oauth/app.ex
Normal file
29
lib/pleroma/web/oauth/app.ex
Normal file
|
@ -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
|
47
lib/pleroma/web/oauth/authorization.ex
Normal file
47
lib/pleroma/web/oauth/authorization.ex
Normal file
|
@ -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
|
49
lib/pleroma/web/oauth/oauth_controller.ex
Normal file
49
lib/pleroma/web/oauth/oauth_controller.ex
Normal file
|
@ -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
|
4
lib/pleroma/web/oauth/oauth_view.ex
Normal file
4
lib/pleroma/web/oauth/oauth_view.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule Pleroma.Web.OAuth.OAuthView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
end
|
38
lib/pleroma/web/oauth/token.ex
Normal file
38
lib/pleroma/web/oauth/token.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
11
lib/pleroma/web/templates/layout/app.html.eex
Normal file
11
lib/pleroma/web/templates/layout/app.html.eex
Normal file
|
@ -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>
|
2
lib/pleroma/web/templates/o_auth/o_auth/results.html.eex
Normal file
2
lib/pleroma/web/templates/o_auth/o_auth/results.html.eex
Normal file
|
@ -0,0 +1,2 @@
|
|||
<h1>Successfully authorized</h1>
|
||||
<h2>Token code is <%= @auth.token %></h2>
|
14
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
Normal file
14
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
Normal file
|
@ -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 %>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} ->
|
||||
|
|
3
lib/pleroma/web/views/layout_view.ex
Normal file
3
lib/pleroma/web/views/layout_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Pleroma.Web.LayoutView do
|
||||
use Pleroma.Web, :view
|
||||
end
|
6
mix.exs
6
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.
|
||||
|
|
7
mix.lock
7
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]}]},
|
||||
|
|
16
priv/repo/migrations/20170906120646_add_mastodon_apps.exs
Normal file
16
priv/repo/migrations/20170906120646_add_mastodon_apps.exs
Normal file
|
@ -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
|
|
@ -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
|
15
priv/repo/migrations/20170906152508_create_o_auth_token.exs
Normal file
15
priv/repo/migrations/20170906152508_create_o_auth_token.exs
Normal file
|
@ -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
|
15
priv/repo/migrations/20170911123607_create_notifications.exs
Normal file
15
priv/repo/migrations/20170911123607_create_notifications.exs
Normal file
|
@ -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
|
23
test/notification_test.exs
Normal file
23
test/notification_test.exs
Normal file
|
@ -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
|
42
test/web/mastodon_api/account_view_test.exs
Normal file
42
test/web/mastodon_api/account_view_test.exs
Normal file
|
@ -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
|
184
test/web/mastodon_api/mastodon_api_controller_test.exs
Normal file
184
test/web/mastodon_api/mastodon_api_controller_test.exs
Normal file
|
@ -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
|
77
test/web/mastodon_api/status_view_test.exs
Normal file
77
test/web/mastodon_api/status_view_test.exs
Normal file
|
@ -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
|
42
test/web/oauth/authorization_test.exs
Normal file
42
test/web/oauth/authorization_test.exs
Normal file
|
@ -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
|
24
test/web/oauth/token_test.exs
Normal file
24
test/web/oauth/token_test.exs
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
|
Loading…
Reference in a new issue