Commit 52200998 authored by lain's avatar lain
Browse files

Merge branch 'develop' into feature/activitypub

parents 4a13b848 0e2ca77e
......@@ -3,20 +3,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.CommonAPI.Utils
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
defp image_url(_), do: nil
alias Pleroma.Web.MediaProxy
def render("accounts.json", %{users: users} = opts) do
render_many(users, AccountView, "account.json", opts)
end
def render("account.json", %{user: user}) do
image = User.avatar_url(user)
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
header = image_url(user.info["banner"]) || "https://placehold.it/700x335"
%{
id: to_string(user.id),
username: hd(String.split(user.nickname, "@")),
......
......@@ -3,6 +3,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
alias Pleroma.{User, Activity}
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("index.json", opts) do
render_many(opts.activities, StatusView, "status.json", opts)
......@@ -121,9 +122,9 @@ def render("attachment.json", %{attachment: attachment}) do
%{
id: to_string(attachment["id"] || hash_id),
url: href,
url: MediaProxy.url(href),
remote_url: href,
preview_url: href,
preview_url: MediaProxy.url(href),
text_url: href,
type: type
}
......
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@max_body_length 25 * 1048576
@cache_control %{
default: "public, max-age=1209600",
error: "public, must-revalidate, max-age=160",
}
def remote(conn, %{"sig" => sig, "url" => url}) do
config = Application.get_env(:pleroma, :media_proxy, [])
with \
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
{:ok, content_type, body} <- proxy_request(url)
do
conn
|> put_resp_content_type(content_type)
|> set_cache_header(:default)
|> send_resp(200, body)
else
false -> send_error(conn, 404)
{:error, :invalid_signature} -> send_error(conn, 403)
{:error, {:http, _, url}} -> redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
end
end
defp proxy_request(link) do
headers = [{"user-agent", "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{Application.get_env(:pleroma, :instance)[:email]}>"}]
options = @httpoison.process_request_options([:insecure, {:follow_redirect, true}])
with \
{:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
headers = Enum.into(headers, Map.new),
{:ok, body} <- proxy_request_body(client),
content_type <- proxy_request_content_type(headers, body)
do
{:ok, content_type, body}
else
{:ok, status, _, _} ->
Logger.warn "MediaProxy: request failed, status #{status}, link: #{link}"
{:error, {:http, :bad_status, link}}
{:error, error} ->
Logger.warn "MediaProxy: request failed, error #{inspect error}, link: #{link}"
{:error, {:http, error, link}}
end
end
defp set_cache_header(conn, key) do
Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
end
defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)
defp send_error(conn, code, body \\ "") do
conn
|> set_cache_header(:error)
|> send_resp(code, body)
end
defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
case :hackney.stream_body(client) do
{:ok, data} -> proxy_request_body(client, <<body :: binary, data :: binary>>)
:done -> {:ok, body}
{:error, reason} -> {:error, reason}
end
end
defp proxy_request_body(client, _) do
:hackney.close(client)
{:error, :body_too_large}
end
# TODO: the body is passed here as well because some hosts do not provide a content-type.
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
defp proxy_request_content_type(headers, _body) do
headers["Content-Type"] || headers["content-type"] || "image/jpeg"
end
end
defmodule Pleroma.Web.MediaProxy do
@base64_opts [padding: false]
def url(nil), do: nil
def url(url = "/" <> _), do: url
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url) do
url
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
Keyword.get(config, :base_url, Pleroma.Web.base_url) <> "/proxy/#{sig64}/#{base64}"
end
end
def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)
local_sig = :crypto.hmac(:sha, secret, url)
if local_sig == sig do
{:ok, Base.url_decode64!(url, @base64_opts)}
else
{:error, :invalid_signature}
end
end
end
defmodule Pleroma.Web.OAuth.FallbackController do
use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.OAuthController
# No user/password
def call(conn, _) do
conn
|> put_flash(:error, "Invalid Username/Password")
|> OAuthController.authorize(conn.params)
end
end
\ No newline at end of file
......@@ -5,6 +5,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.{Repo, User}
alias Comeonin.Pbkdf2
plug :fetch_session
plug :fetch_flash
action_fallback Pleroma.Web.OAuth.FallbackController
def authorize(conn, params) do
render conn, "show.html", %{
response_type: params["response_type"],
......
defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
alias Pleroma.User
alias Pleroma.Web.MediaProxy
def to_simple_form(user, activities, _users) do
most_recent_update = (List.first(activities) || user).updated_at
......@@ -25,6 +27,7 @@ def to_simple_form(user, activities, _users) do
{:id, h.(OStatus.feed_path(user))},
{:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)},
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},
......
......@@ -22,6 +22,10 @@ def salmon_path(user) do
"#{user.ap_id}/salmon"
end
def remote_follow_path do
"#{Web.base_url}/ostatus_subscribe?acct={uri}"
end
def handle_incoming(xml_string) do
with doc when doc != :error <- parse_document(xml_string) do
entries = :xmerl_xpath.string('//entry', doc)
......@@ -159,8 +163,7 @@ def get_content(entry) do
Get the cw that mastodon uses.
"""
def get_cw(entry) do
with scope when not is_nil(scope) <- string_from_xpath("//mastodon:scope", entry),
cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw
else _e -> nil
end
......
......@@ -28,6 +28,13 @@ def user_fetcher(username) do
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
end
pipeline :pleroma_html do
plug :accepts, ["html"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
end
pipeline :well_known do
plug :accepts, ["xml", "xrd+xml"]
end
......@@ -51,6 +58,18 @@ def user_fetcher(username) do
get "/emoji", UtilController, :emoji
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_html
get "/ostatus_subscribe", UtilController, :remote_follow
post "/ostatus_subscribe", UtilController, :do_remote_follow
post "/main/ostatus", UtilController, :remote_subscribe
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :authenticated_api
post "/follow_import", UtilController, :follow_import
end
scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize
post "/authorize", OAuthController, :create_authorization
......@@ -101,6 +120,7 @@ def user_fetcher(username) do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :api
get "/instance", MastodonAPIController, :masto_instance
get "/instance/peers", MastodonAPIController, :peers
post "/apps", MastodonAPIController, :create_app
get "/custom_emojis", MastodonAPIController, :custom_emojis
......@@ -142,6 +162,8 @@ def user_fetcher(username) do
get "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/users/show", TwitterAPI.Controller, :show_user
get "/statuses/followers", TwitterAPI.Controller, :followers
get "/statuses/friends", TwitterAPI.Controller, :friends
get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status
get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation
......@@ -188,8 +210,6 @@ def user_fetcher(username) do
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar
get "/statuses/followers", TwitterAPI.Controller, :followers
get "/statuses/friends", TwitterAPI.Controller, :friends
get "/friends/ids", TwitterAPI.Controller, :friends_ids
get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array
......@@ -243,6 +263,14 @@ def user_fetcher(username) do
delete "/auth/sign_out", MastodonAPIController, :logout
end
pipeline :remote_media do
plug :accepts, ["html"]
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through :remote_media
get "/:sig/:url", MediaProxyController, :remote
end
scope "/", Fallback do
get "/*path", RedirectController, :redirector
end
......
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<%= label f, :name, "Name" %>
......
<%= if @error == :error do %>
<h2>Error fetching user</h2>
<% else %>
<h2>Remote follow</h2>
<img width="128" height="128" src="<%= @avatar %>">
<p><%= @name %></p>
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @id %>
<%= submit "Authorize" %>
<% end %>
<% end %>
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2>Log in to follow</h2>
<p><%= @name %></p>
<img height="128" width="128" src="<%= @avatar %>">
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username" %>
<br>
<%= password_input f, :password, placeholder: "Password" %>
<br>
<%= hidden_input f, :id, value: @id %>
<%= submit "Authorize" %>
<% end %>
<%= if @error do %>
<p>Error following account</p>
<% else %>
<h2>Account followed!</h2>
<% end %>
<%= if @error do %>
<h2>Error: <%= @error %></h2>
<% else %>
<h2>Remotely follow <%= @nickname %></h2>
<%= form_for @conn, util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
<%= hidden_input f, :nickname, value: @nickname %>
<%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
<%= submit "Follow" %>
<% end %>
<% end %>
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Web
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User}
def show_password_reset(conn, %{"token" => token}) do
......@@ -29,6 +33,72 @@ def help_test(conn, _params) do
json(conn, "ok")
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else
_e -> render(conn, "subscribe.html", %{nickname: nick, avatar: nil, error: "Could not find user"})
end
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{nickname: nick, avatar: nil, error: "Something went wrong."})
end
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
conn
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
else
conn
|> render("follow_login.html", %{error: false, acct: acct, avatar: avatar, name: name, id: id})
end
end
def do_remote_follow(conn, %{"authorization" => %{"name" => username, "password" => password, "id" => id}}) do
followee = Repo.get(User, id)
avatar = User.avatar_url(followee)
name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash),
%User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
_e ->
conn
|> render("follow_login.html", %{error: "Wrong username or password", id: id, name: name, avatar: avatar})
end
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
e ->
Logger.debug("Remote follow failed with error #{inspect e}")
conn
|> render("followed.html", %{error: inspect(e)})
end
end
@instance Application.get_env(:pleroma, :instance)
def config(conn, _params) do
case get_format(conn) do
......@@ -51,7 +121,7 @@ def config(conn, _params) do
site: %{
name: Keyword.get(@instance, :name),
server: Web.base_url,
textlimit: Keyword.get(@instance, :limit),
textlimit: to_string(Keyword.get(@instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1")
}
})
......@@ -73,4 +143,24 @@ def version(conn, _params) do
def emoji(conn, _params) do
json conn, Enum.into(Formatter.get_custom_emoji(), %{})
end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
follow_import(conn, %{"list" => File.read!(listfile.path)})
end
def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
Task.start(fn ->
String.split(list)
|> Enum.map(fn nick ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch_by_nickname(nick),
{:ok, follower} <- User.follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
_e -> Logger.debug "follow_import: following #{nick} failed"
end
end)
end)
json conn, "job started"
end
end
......@@ -6,7 +6,7 @@ def to_map(%Object{} = object, _opts) do
data = object.data
url = List.first(data["url"])
%{
url: url["href"],
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"],
id: data["uuid"],
oembed: false
......
......@@ -316,10 +316,12 @@ def conversation_id_to_context(id) do
def get_external_profile(for_user, uri) do
with {:ok, %User{} = user} <- OStatus.find_or_make_user(uri) do
with url <- user.info["topic"],
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
OStatus.handle_incoming(body)
end
spawn(fn ->
with url <- user.info["topic"],
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
OStatus.handle_incoming(body)
end
end)
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else _e ->
{:error, "Couldn't find user"}
......
......@@ -263,16 +263,18 @@ def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" =>
end
end
def followers(%{assigns: %{user: user}} = conn, _params) do
with {:ok, followers} <- User.get_followers(user) do
def followers(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns.user, params),
{:ok, followers} <- User.get_followers(user) do
render(conn, UserView, "index.json", %{users: followers, for: user})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
def friends(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do
def friends(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns.user, params),
{:ok, friends} <- User.get_friends(user) do
render(conn, UserView, "index.json", %{users: friends, for: user})
else
_e -> bad_request_reply(conn, "Can't get friends")
......
......@@ -2,6 +2,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
......@@ -12,7 +13,7 @@ def render("index.json", %{users: users, for: user}) do
end
def render("user.json", %{user: user = %User{}} = assigns) do
image = User.avatar_url(user)
image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} = if assigns[:for] do
{
User.following?(assigns[:for], user),
......@@ -44,8 +45,9 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => image_url(user.info["banner"]),
"background_image" => image_url(user.info["background"])
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
"is_local" => user.local
}
if assigns[:token] do
......
......@@ -45,7 +45,8 @@ def represent_user(user) do
{:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}}
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
]
}
|> XmlBuilder.to_doc
......@@ -69,11 +70,13 @@ defp webfinger_from_xml(doc) do
topic = XML.string_from_xpath(~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href}, doc)
subject = XML.string_from_xpath("//Subject", doc)
salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc)
subscribe_address = XML.string_from_xpath(~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, doc)
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon
"salmon" => salmon,
"subscribe_address" => subscribe_address