X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fmastodon_api%2Fmastodon_api_controller.ex;h=ce30453cb57de21994bdd25a9b83cc2b89858771;hb=a4335e391f6818b9ea09520f60ca8c436d643f38;hp=8547606dece264ee7f5b110bb5160ddc7a1134c0;hpb=94be93aad08ee9b1d43b1a67d94b694101bf5f1a;p=akkoma diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 8547606de..ce30453cb 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,14 +1,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo, Activity, User, Notification} - alias Pleroma.Web.OAuth.App + alias Pleroma.{Repo, Activity, User, Notification, Stats} alias Pleroma.Web - alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} + alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView} alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.Web.CommonAPI + alias Pleroma.Web.{CommonAPI, OStatus} + alias Pleroma.Web.OAuth.{Authorization, Token, App} + alias Comeonin.Pbkdf2 import Ecto.Query - import Logger + require Logger def create_app(conn, params) do with cs <- App.register_changeset(%App{}, params) |> IO.inspect, @@ -23,22 +23,114 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def verify_credentials(%{assigns: %{user: user}} = conn, params) do + def update_credentials(%{assigns: %{user: user}} = conn, params) do + params = if bio = params["note"] do + Map.put(params, "bio", bio) + else + params + end + + params = if name = params["display_name"] do + Map.put(params, "name", name) + else + params + end + + user = if avatar = params["avatar"] do + with %Plug.Upload{} <- avatar, + {:ok, object} <- ActivityPub.upload(avatar), + change = Ecto.Changeset.change(user, %{avatar: object.data}), + {:ok, user} = Repo.update(change) do + user + else + _e -> user + end + else + user + end + + user = if banner = params["header"] do + with %Plug.Upload{} <- banner, + {:ok, object} <- ActivityPub.upload(banner), + new_info <- Map.put(user.info, "banner", object.data), + change <- User.info_changeset(user, %{info: new_info}), + {:ok, user} <- Repo.update(change) do + user + else + _e -> user + end + else + user + end + + with changeset <- User.update_changeset(user, params), + {:ok, user} <- Repo.update(changeset) do + json conn, AccountView.render("account.json", %{user: user}) + else + _e -> + conn + |> put_status(403) + |> json(%{error: "Invalid request"}) + end + end + + def verify_credentials(%{assigns: %{user: user}} = conn, _) do account = AccountView.render("account.json", %{user: user}) json(conn, account) end + def user(conn, %{"id" => id}) do + with %User{} = user <- Repo.get(User, id) do + account = AccountView.render("account.json", %{user: user}) + json(conn, account) + else + _e -> conn + |> put_status(404) + |> json(%{error: "Can't find user"}) + end + end + + @instance Application.get_env(:pleroma, :instance) + def masto_instance(conn, _params) do response = %{ uri: Web.base_url, - title: Web.base_url, + title: Keyword.get(@instance, :name), description: "A Pleroma instance, an alternative fediverse server", - version: "Pleroma Dev" + version: Keyword.get(@instance, :version), + email: Keyword.get(@instance, :email), + urls: %{ + streaming_api: String.replace(Web.base_url, ["http","https"], "wss") + }, + stats: Stats.get_stats, + thumbnail: Web.base_url <> "/static/instance-thumbnail.jpeg", + max_toot_chars: Keyword.get(@instance, :limit) } json(conn, response) end + def peers(conn, _params) do + json(conn, Stats.get_peers) + end + + defp mastodonized_emoji do + Pleroma.Formatter.get_custom_emoji() + |> Enum.map(fn {shortcode, relative_url} -> + url = to_string URI.merge(Web.base_url(), relative_url) + %{ + "shortcode" => shortcode, + "static_url" => url, + "url" => url + } + end) + end + + def custom_emojis(conn, _params) do + mastodon_emoji = mastodonized_emoji() + json conn, mastodon_emoji + end + defp add_link_headers(conn, method, activities) do last = List.last(activities) first = List.first(activities) @@ -55,7 +147,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def home_timeline(%{assigns: %{user: user}} = conn, params) do - activities = ActivityPub.fetch_activities([user.ap_id | user.following], Map.put(params, "type", "Create")) + params = params + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("blocking_user", user) + + activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) |> Enum.reverse conn @@ -65,8 +161,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def public_timeline(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.put("type", "Create") - |> Map.put("local_only", !!params["local"]) + |> Map.put("type", ["Create", "Announce"]) + |> Map.put("local_only", params["local"] in [true, "True", "true", "1"]) + |> Map.put("blocking_user", user) activities = ActivityPub.fetch_public_activities(params) |> Enum.reverse @@ -76,11 +173,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) end + # TODO: Link headers 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("type", ["Create", "Announce"]) |> Map.put("actor_id", ap_id) + |> Map.put("whole_db", true) activities = ActivityPub.fetch_activities([], params) |> Enum.reverse @@ -97,8 +196,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Repo.get(Activity, id), - activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"]), + activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"], %{"blocking_user" => user}), activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), + activities <- activities |> Enum.filter(fn (%{data: %{"type" => type}}) -> type == "Create" 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, @@ -109,16 +209,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params) do - l = status |> String.trim |> String.length - + def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params = params |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) + |> Map.put("no_attachment_links", true) - 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 + {:ok, activity} = CommonAPI.post(user, params) + render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} end def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do @@ -133,9 +230,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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} + with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do + render conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity} end end @@ -155,23 +251,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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"]) - created_at = NaiveDateTime.to_iso8601(created_at) - |> String.replace(~r/\.\d+Z/, ".000Z") - 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 + result = Enum.map(notifications, fn x -> + render_notification(user, x) end) |> Enum.filter(&(&1)) @@ -180,6 +261,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> json(result) end + def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + with {:ok, notification} <- Notification.get(user, id) do + json(conn, render_notification(user, notification)) + else + {:error, reason} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => reason})) + end + end + + def clear_notifications(%{assigns: %{user: user}} = conn, _params) do + Notification.clear(user) + json(conn, %{}) + end + + def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + with {:ok, _notif} <- Notification.dismiss(user, id) do + json(conn, %{}) + else + {:error, reason} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => reason})) + end + end + def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do id = List.wrap(id) q = from u in User, @@ -188,8 +296,334 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do render conn, AccountView, "relationships.json", %{user: user, targets: targets} end + def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do + with {:ok, object} <- ActivityPub.upload(file) do + data = object.data + |> Map.put("id", object.id) + + render conn, StatusView, "attachment.json", %{attachment: data} + end + end + + def favourited_by(conn, %{"id" => id}) do + with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do + q = from u in User, + where: u.ap_id in ^likes + users = Repo.all(q) + render conn, AccountView, "accounts.json", %{users: users, as: :user} + else + _ -> json(conn, []) + end + end + + def reblogged_by(conn, %{"id" => id}) do + with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do + q = from u in User, + where: u.ap_id in ^announces + users = Repo.all(q) + render conn, AccountView, "accounts.json", %{users: users, as: :user} + else + _ -> json(conn, []) + end + end + + # TODO: Link headers + def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do + params = params + |> Map.put("type", "Create") + |> Map.put("local_only", !!params["local"]) + |> Map.put("blocking_user", user) + + activities = ActivityPub.fetch_public_activities(params) + |> Enum.reverse + + conn + |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) + end + + # TODO: Pagination + def followers(conn, %{"id" => id}) do + with %User{} = user <- Repo.get(User, id), + {:ok, followers} <- User.get_followers(user) do + render conn, AccountView, "accounts.json", %{users: followers, as: :user} + end + end + + def following(conn, %{"id" => id}) do + with %User{} = user <- Repo.get(User, id), + {:ok, followers} <- User.get_friends(user) do + render conn, AccountView, "accounts.json", %{users: followers, as: :user} + end + end + + def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do + with %User{} = followed <- Repo.get(User, id), + {:ok, follower} <- User.follow(follower, followed), + {:ok, _activity} <- ActivityPub.follow(follower, followed) do + render conn, AccountView, "relationship.json", %{user: follower, target: followed} + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do + with %User{} = followed <- Repo.get_by(User, nickname: uri), + {:ok, follower} <- User.follow(follower, followed), + {:ok, _activity} <- ActivityPub.follow(follower, followed) do + render conn, AccountView, "account.json", %{user: followed} + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + # TODO: Clean up and unify + def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do + with %User{} = followed <- Repo.get(User, id), + { :ok, follower, follow_activity } <- User.unfollow(follower, followed), + { :ok, _activity } <- ActivityPub.insert(%{ + "type" => "Undo", + "actor" => follower.ap_id, + "object" => follow_activity.data["id"] # get latest Follow for these users + }) do + render conn, AccountView, "relationship.json", %{user: follower, target: followed} + end + end + + def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- Repo.get(User, id), + {:ok, blocker} <- User.block(blocker, blocked) do + render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- Repo.get(User, id), + {:ok, blocker} <- User.unblock(blocker, blocked) do + render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + # TODO: Use proper query + def blocks(%{assigns: %{user: user}} = conn, _) do + with blocked_users <- user.info["blocks"] || [], + accounts <- Enum.map(blocked_users, fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) do + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + json(conn, res) + end + end + + def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, params["resolve"] == "true") + + fetched = if Regex.match?(~r/https?:/, query) do + with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do + activities + else + _e -> [] + end + end || [] + + q = from a in Activity, + where: fragment("?->>'type' = 'Create'", a.data), + where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query), + limit: 20 + statuses = Repo.all(q) ++ fetched + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => [] + } + + json(conn, res) + end + + def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, params["resolve"] == "true") + + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + + json(conn, res) + end + + def favourites(%{assigns: %{user: user}} = conn, _) do + params = conn + |> Map.put("type", "Create") + |> Map.put("favorited_by", user.ap_id) + |> Map.put("blocking_user", user) + + activities = ActivityPub.fetch_activities([], params) + |> Enum.reverse + + conn + |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) + end + + def index(%{assigns: %{user: user}} = conn, _params) do + token = conn + |> get_session(:oauth_token) + + if user && token do + mastodon_emoji = mastodonized_emoji() + accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user})) + initial_state = %{ + meta: %{ + streaming_api_base_url: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), + access_token: token, + locale: "en", + domain: Pleroma.Web.Endpoint.host(), + admin: "1", + me: "#{user.id}", + unfollow_modal: false, + boost_modal: false, + delete_modal: true, + auto_play_gif: false, + reduce_motion: false + }, + compose: %{ + me: "#{user.id}", + default_privacy: "public", + default_sensitive: false + }, + media_attachments: %{ + accept_content_types: [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webm", + ".mp4", + ".m4v", + "image\/jpeg", + "image\/png", + "image\/gif", + "video\/webm", + "video\/mp4" + ] + }, + settings: %{ + onboarded: true, + home: %{ + shows: %{ + reblog: true, + reply: true + } + }, + notifications: %{ + alerts: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + shows: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + sounds: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + } + } + }, + push_subscription: nil, + accounts: accounts, + custom_emojis: mastodon_emoji + } |> Poison.encode! + conn + |> put_layout(false) + |> render(MastodonView, "index.html", %{initial_state: initial_state}) + else + conn + |> redirect(to: "/web/login") + end + end + + def login(conn, _) do + conn + |> render(MastodonView, "login.html", %{error: false}) + end + + defp get_or_make_app() do + with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do + {:ok, app} + else + _e -> + cs = App.register_changeset(%App{}, %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: "read,write,follow"}) + Repo.insert(cs) + end + end + + def login_post(conn, %{"authorization" => %{ "name" => name, "password" => password}}) do + with %User{} = user <- User.get_cached_by_nickname(name), + true <- Pbkdf2.checkpw(password, user.password_hash), + {:ok, app} <- get_or_make_app(), + {:ok, auth} <- Authorization.create_authorization(app, user), + {:ok, token} <- Token.exchange_token(app, auth) do + conn + |> put_session(:oauth_token, token.token) + |> redirect(to: "/web/getting-started") + else + _e -> + conn + |> render(MastodonView, "login.html", %{error: "Wrong username or password"}) + end + end + + def logout(conn, _) do + conn + |> clear_session + |> redirect(to: "/") + end + + def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do + Logger.debug("Unimplemented, returning unmodified relationship") + with %User{} = target <- Repo.get(User, id) do + render conn, AccountView, "relationship.json", %{user: user, target: target} + end + end + def empty_array(conn, _) do Logger.debug("Unimplemented, returning an empty array") json(conn, []) end + + def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do + actor = User.get_cached_by_ap_id(activity.data["actor"]) + created_at = NaiveDateTime.to_iso8601(created_at) + |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) + 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, for: user})} + "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, for: user})} + "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, for: user})} + "Follow" -> + %{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})} + _ -> nil + end + end end