Merge branch 'develop' into feature/activitypub

This commit is contained in:
Roger Braun 2018-02-11 09:50:55 +01:00
commit 52200998c9
101 changed files with 661 additions and 146 deletions

View file

@ -22,7 +22,7 @@ No release has been made yet, but several servers have been online for months al
### Dependencies ### Dependencies
* Postgresql version 9.6 or newer * Postgresql version 9.6 or newer
* Elixir version 1.4 or newer (you will also need erlang-dev, erlang-parsetools, erlang-xmerl packages) * Elixir version 1.5 or newer
* Build-essential tools * Build-essential tools
### Configuration ### Configuration
@ -50,3 +50,12 @@ Logs can be watched by using `journalctl -fu pleroma.service`
### Standalone/run by other means ### Standalone/run by other means
Run `mix phx.server` in repository's root, it will output log into stdout/stderr Run `mix phx.server` in repository's root, it will output log into stdout/stderr
### Using an upstream proxy for federation
Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that pleroma makes to an upstream proxy server:
config :pleroma, :http,
proxy_url: "127.0.0.1:8123"
This is useful for running pleroma inside Tor or i2p.

View file

@ -33,7 +33,7 @@
config :pleroma, :websub, Pleroma.Web.Websub config :pleroma, :websub, Pleroma.Web.Websub
config :pleroma, :ostatus, Pleroma.Web.OStatus config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, HTTPoison config :pleroma, :httpoison, Pleroma.HTTP
version = with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do version = with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do
"Pleroma #{String.trim(version)}" "Pleroma #{String.trim(version)}"
@ -41,6 +41,10 @@
_ -> "Pleroma dev" _ -> "Pleroma dev"
end end
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil
config :pleroma, :instance, config :pleroma, :instance,
version: version, version: version,
name: "Pleroma", name: "Pleroma",
@ -48,6 +52,14 @@
limit: 5000, limit: 5000,
registrations_open: true registrations_open: true
config :pleroma, :media_proxy,
enabled: false,
redirect_on_failure: true
#base_url: "https://cache.pleroma.social"
config :pleroma, :chat,
enabled: true
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs" import_config "#{Mix.env}.exs"

View file

@ -1 +1,31 @@
firefox, /emoji/Firefox.gif firefox, /emoji/Firefox.gif
blank, /emoji/blank.png
f_00b, /emoji/f_00b.png
f_00b11b, /emoji/f_00b11b.png
f_00b33b, /emoji/f_00b33b.png
f_00h, /emoji/f_00h.png
f_00t, /emoji/f_00t.png
f_01b, /emoji/f_01b.png
f_03b, /emoji/f_03b.png
f_10b, /emoji/f_10b.png
f_11b, /emoji/f_11b.png
f_11b00b, /emoji/f_11b00b.png
f_11b22b, /emoji/f_11b22b.png
f_11h, /emoji/f_11h.png
f_11t, /emoji/f_11t.png
f_12b, /emoji/f_12b.png
f_21b, /emoji/f_21b.png
f_22b, /emoji/f_22b.png
f_22b11b, /emoji/f_22b11b.png
f_22b33b, /emoji/f_22b33b.png
f_22h, /emoji/f_22h.png
f_22t, /emoji/f_22t.png
f_23b, /emoji/f_23b.png
f_30b, /emoji/f_30b.png
f_32b, /emoji/f_32b.png
f_33b, /emoji/f_33b.png
f_33b00b, /emoji/f_33b00b.png
f_33b22b, /emoji/f_33b22b.png
f_33h, /emoji/f_33h.png
f_33t, /emoji/f_33t.png

View file

@ -14,9 +14,12 @@
# manifest is generated by the mix phoenix.digest task # manifest is generated by the mix phoenix.digest task
# which you typically run after static files are built. # which you typically run after static files are built.
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
on_init: {Pleroma.Web.Endpoint, :load_from_system_env, []}, http: [port: 4000],
url: [host: "example.com", port: 80], protocol: "http",
cache_static_manifest: "priv/static/cache_manifest.json" debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: []
# Do not print debug messages in production # Do not print debug messages in production
config :logger, level: :info config :logger, level: :info

5
installation/Caddyfile Normal file
View file

@ -0,0 +1,5 @@
instance.example.com { # Your instance's domain
proxy / localhost:4000 {
websocket
}
}

View file

@ -1,3 +1,6 @@
proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cache:10m max_size=10g
inactive=720m use_temp_path=off;
server { server {
listen 80; listen 80;
server_name example.tld; server_name example.tld;
@ -19,11 +22,17 @@ server {
server_name example.tld; server_name example.tld;
location / { location / {
add_header 'Access-Control-Allow-Origin' '*';
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_pass http://localhost:4000; proxy_pass http://localhost:4000;
} }
include snippets/well-known.conf;
location /proxy {
proxy_cache pleroma_media_cache;
proxy_cache_lock on;
proxy_pass http://localhost:4000;
}
} }

View file

@ -8,11 +8,20 @@ def run(_) do
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim
email = IO.gets("What's your admin email address: ") |> String.trim email = IO.gets("What's your admin email address: ") |> String.trim
mediaproxy = IO.gets("Do you want to activate the mediaproxy? (y/N): ")
|> String.trim()
|> String.downcase()
|> String.starts_with?("y")
proxy_url = if mediaproxy do
IO.gets("What is the mediaproxy's URL? (e.g. https://cache.example.com): ") |> String.trim
else
"https://cache.example.com"
end
secret = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64) secret = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64) dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", [dbpass: dbpass]) resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", [dbpass: dbpass])
result = EEx.eval_file("lib/mix/tasks/sample_config.eex", [domain: domain, email: email, name: name, secret: secret, dbpass: dbpass]) result = EEx.eval_file("lib/mix/tasks/sample_config.eex", [domain: domain, email: email, name: name, secret: secret, mediaproxy: mediaproxy, proxy_url: proxy_url, dbpass: dbpass])
IO.puts("\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs") IO.puts("\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs")
File.write("config/generated_config.exs", result) File.write("config/generated_config.exs", result)

View file

@ -10,6 +10,11 @@ config :pleroma, :instance,
limit: 5000, limit: 5000,
registrations_open: true registrations_open: true
config :pleroma, :media_proxy,
enabled: <%= mediaproxy %>,
redirect_on_failure: true,
base_url: "<%= proxy_url %>"
# Configure your database # Configure your database
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres, adapter: Ecto.Adapters.Postgres,

View file

@ -20,13 +20,18 @@ def start(_type, _args) do
limit: 2500 limit: 2500
]]), ]]),
worker(Pleroma.Web.Federator, []), worker(Pleroma.Web.Federator, []),
worker(Pleroma.Web.ChatChannel.ChatChannelState, []), worker(Pleroma.Stats, []),
] ]
++ if Mix.env == :test, do: [], else: [worker(Pleroma.Web.Streamer, [])] ++ if Mix.env == :test, do: [], else: [worker(Pleroma.Web.Streamer, [])]
++ if !chat_enabled(), do: [], else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor] opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
defp chat_enabled do
Application.get_env(:pleroma, :chat, []) |> Keyword.get(:enabled)
end
end end

View file

@ -1,5 +1,6 @@
defmodule Pleroma.Formatter do defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy
@link_regex ~r/https?:\/\/[\w\.\/?=\-#%&@~\(\)]+[\w\/]/u @link_regex ~r/https?:\/\/[\w\.\/?=\-#%&@~\(\)]+[\w\/]/u
def linkify(text) do def linkify(text) do
@ -10,7 +11,7 @@ def linkify(text) do
def parse_tags(text, data \\ %{}) do def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text) Regex.scan(@tag_regex, text)
|> Enum.map(fn (["#" <> tag = full_tag]) -> {full_tag, String.downcase(tag)} end) |> Enum.map(fn (["#" <> tag = full_tag]) -> {full_tag, String.downcase(tag)} end)
|> (fn map -> if data["sensitive"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).() |> (fn map -> if data["sensitive"] in [true, "True", "true", "1"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).()
end end
def parse_mentions(text) do def parse_mentions(text) do
@ -103,12 +104,18 @@ def html_escape(text) do
{finmoji, "/finmoji/128px/#{finmoji}-128.png"} {finmoji, "/finmoji/128px/#{finmoji}-128.png"}
end) end)
@emoji_from_file (with {:ok, file} <- File.read("config/emoji.txt") do @emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do
file custom =
|> String.trim with {:ok, custom} <- File.read("config/custom_emoji.txt") do
|> String.split("\n") custom
else
_e -> ""
end
(default <> "\n" <> custom)
|> String.trim()
|> String.split(~r/\n+/)
|> Enum.map(fn(line) -> |> Enum.map(fn(line) ->
[name, file] = String.split(line, ", ") [name, file] = String.split(line, ~r/,\s*/)
{name, file} {name, file}
end) end)
else else
@ -125,7 +132,7 @@ def emojify(text, additional \\ nil) do
end end
Enum.reduce(all_emoji, text, fn ({emoji, file}, text) -> Enum.reduce(all_emoji, text, fn ({emoji, file}, text) ->
String.replace(text, ":#{emoji}:", "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{file}' />") String.replace(text, ":#{emoji}:", "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />")
end) end)
end end

14
lib/pleroma/http/http.ex Normal file
View file

@ -0,0 +1,14 @@
defmodule Pleroma.HTTP do
use HTTPoison.Base
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil)
case proxy do
nil -> options
_ -> options ++ [proxy: proxy]
end
end
end

41
lib/pleroma/stats.ex Normal file
View file

@ -0,0 +1,41 @@
defmodule Pleroma.Stats do
import Ecto.Query
alias Pleroma.{User, Repo, Activity}
def start_link do
agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
spawn(fn -> schedule_update() end)
agent
end
def get_stats do
Agent.get(__MODULE__, fn {_, stats} -> stats end)
end
def get_peers do
Agent.get(__MODULE__, fn {peers, _} -> peers end)
end
def schedule_update do
spawn(fn ->
Process.sleep(1000 * 60 * 60 * 1) # 1 hour
schedule_update()
end)
update_stats()
end
def update_stats do
peers = from(u in Pleroma.User,
select: fragment("distinct ?->'host'", u.info),
where: u.local != ^true)
|> Repo.all()
domain_count = Enum.count(peers)
status_query = from(u in User.local_user_query,
select: fragment("sum((?->>'note_count')::int)", u.info))
status_count = Repo.one(status_query) |> IO.inspect
user_count = Repo.aggregate(User.local_user_query, :count, :id)
Agent.update(__MODULE__, fn _ ->
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}
end)
end
end

View file

@ -9,7 +9,7 @@ def store(%Plug.Upload{} = file) do
File.cp!(file.path, result_file) File.cp!(file.path, result_file)
# fix content type on some image uploads # fix content type on some image uploads
content_type = if file.content_type == "application/octet-stream" do content_type = if file.content_type in [nil, "application/octet-stream"] do
get_content_type(file.path) get_content_type(file.path)
else else
file.content_type file.content_type

View file

@ -29,14 +29,14 @@ defmodule Pleroma.User do
def avatar_url(user) do def avatar_url(user) do
case user.avatar do case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href %{"url" => [%{"href" => href} | _]} -> href
_ -> "https://placehold.it/48x48" _ -> "#{Web.base_url()}/images/avi.png"
end end
end end
def banner_url(user) do def banner_url(user) do
case user.info["banner"] do case user.info["banner"] do
%{"url" => [%{"href" => href} | _]} -> href %{"url" => [%{"href" => href} | _]} -> href
_ -> nil _ -> "#{Web.base_url()}/images/banner.png"
end end
end end

View file

@ -5,7 +5,9 @@ defmodule Pleroma.Web.UserSocket do
## Channels ## Channels
# channel "room:*", Pleroma.Web.RoomChannel # channel "room:*", Pleroma.Web.RoomChannel
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
channel "chat:*", Pleroma.Web.ChatChannel channel "chat:*", Pleroma.Web.ChatChannel
end
## Transports ## Transports
transport :websocket, Phoenix.Transports.WebSocket transport :websocket, Phoenix.Transports.WebSocket

View file

@ -24,7 +24,6 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}}
end end
defmodule Pleroma.Web.ChatChannel.ChatChannelState do defmodule Pleroma.Web.ChatChannel.ChatChannelState do
use Agent
@max_messages 20 @max_messages 20
def start_link do def start_link do

View file

@ -95,7 +95,7 @@ def add_user_links(text, mentions) do
Enum.reduce(mentions, step_one, fn ({match, %User{ap_id: ap_id}, uuid}, text) -> Enum.reduce(mentions, step_one, fn ({match, %User{ap_id: ap_id}, uuid}, text) ->
short_match = String.split(match, "@") |> tl() |> hd() short_match = String.split(match, "@") |> tl() |> hd()
String.replace(text, uuid, "<a href='#{ap_id}'>@#{short_match}</a>") String.replace(text, uuid, "<span><a href='#{ap_id}'>@<span>#{short_match}</span></a></span>")
end) end)
end end

View file

@ -1,7 +1,9 @@
defmodule Pleroma.Web.Endpoint do defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma use Phoenix.Endpoint, otp_app: :pleroma
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
socket "/socket", Pleroma.Web.UserSocket socket "/socket", Pleroma.Web.UserSocket
end
socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
@ -12,7 +14,7 @@ defmodule Pleroma.Web.Endpoint do
at: "/media", from: "uploads", gzip: false at: "/media", from: "uploads", gzip: false
plug Plug.Static, plug Plug.Static,
at: "/", from: :pleroma, at: "/", from: :pleroma,
only: ~w(index.html static finmoji emoji packs sounds sw.js) only: ~w(index.html static finmoji emoji packs sounds images instance sw.js)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.

View file

@ -41,12 +41,12 @@ def handle(:request_subscription, websub) do
def handle(:publish, activity) do def handle(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
Logger.debug(fn -> "Sending #{activity.data["id"]} out via salmon" end) Logger.debug(fn -> "Sending #{activity.data["id"]} out via salmon" end)
Pleroma.Web.Salmon.publish(actor, activity) Pleroma.Web.Salmon.publish(actor, activity)
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
end end
end end

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.{Repo, Activity, User, Notification, Stats}
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -93,7 +93,6 @@ def user(conn, %{"id" => id}) do
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
def masto_instance(conn, _params) do def masto_instance(conn, _params) do
user_count = Repo.aggregate(User.local_user_query, :count, :id)
response = %{ response = %{
uri: Web.base_url, uri: Web.base_url,
title: Keyword.get(@instance, :name), title: Keyword.get(@instance, :name),
@ -103,17 +102,18 @@ def masto_instance(conn, _params) do
urls: %{ urls: %{
streaming_api: String.replace(Web.base_url, ["http","https"], "wss") streaming_api: String.replace(Web.base_url, ["http","https"], "wss")
}, },
stats: %{ stats: Stats.get_stats,
status_count: 2, thumbnail: Web.base_url <> "/instance/thumbnail.jpeg",
user_count: user_count,
domain_count: 3
},
max_toot_chars: Keyword.get(@instance, :limit) max_toot_chars: Keyword.get(@instance, :limit)
} }
json(conn, response) json(conn, response)
end end
def peers(conn, _params) do
json(conn, Stats.get_peers)
end
defp mastodonized_emoji do defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji() Pleroma.Formatter.get_custom_emoji()
|> Enum.map(fn {shortcode, relative_url} -> |> Enum.map(fn {shortcode, relative_url} ->
@ -162,7 +162,7 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
def public_timeline(%{assigns: %{user: user}} = conn, params) do def public_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", !!params["local"]) |> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities = ActivityPub.fetch_public_activities(params)

View file

@ -3,20 +3,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
defp image_url(_), do: nil
def render("accounts.json", %{users: users} = opts) do def render("accounts.json", %{users: users} = opts) do
render_many(users, AccountView, "account.json", opts) render_many(users, AccountView, "account.json", opts)
end end
def render("account.json", %{user: user}) do 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) user_info = User.user_info(user)
header = image_url(user.info["banner"]) || "https://placehold.it/700x335"
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),

View file

@ -3,6 +3,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.{AccountView, StatusView} alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
alias Pleroma.{User, Activity} alias Pleroma.{User, Activity}
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("index.json", opts) do def render("index.json", opts) do
render_many(opts.activities, StatusView, "status.json", opts) 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), id: to_string(attachment["id"] || hash_id),
url: href, url: MediaProxy.url(href),
remote_url: href, remote_url: href,
preview_url: href, preview_url: MediaProxy.url(href),
text_url: href, text_url: href,
type: type type: type
} }

View file

@ -0,0 +1,84 @@
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

View file

@ -0,0 +1,32 @@
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

View file

@ -0,0 +1,12 @@
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

View file

@ -5,6 +5,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
plug :fetch_session
plug :fetch_flash
action_fallback Pleroma.Web.OAuth.FallbackController
def authorize(conn, params) do def authorize(conn, params) do
render conn, "show.html", %{ render conn, "show.html", %{
response_type: params["response_type"], response_type: params["response_type"],

View file

@ -1,6 +1,8 @@
defmodule Pleroma.Web.OStatus.FeedRepresenter do defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter} alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
alias Pleroma.User
alias Pleroma.Web.MediaProxy
def to_simple_form(user, activities, _users) do def to_simple_form(user, activities, _users) do
most_recent_update = (List.first(activities) || user).updated_at 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))}, {:id, h.(OStatus.feed_path(user))},
{:title, ['#{user.nickname}\'s timeline']}, {:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)}, {: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: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []}, {:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []}, {:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},

View file

@ -22,6 +22,10 @@ def salmon_path(user) do
"#{user.ap_id}/salmon" "#{user.ap_id}/salmon"
end end
def remote_follow_path do
"#{Web.base_url}/ostatus_subscribe?acct={uri}"
end
def handle_incoming(xml_string) do def handle_incoming(xml_string) do
with doc when doc != :error <- parse_document(xml_string) do with doc when doc != :error <- parse_document(xml_string) do
entries = :xmerl_xpath.string('//entry', doc) entries = :xmerl_xpath.string('//entry', doc)
@ -159,8 +163,7 @@ def get_content(entry) do
Get the cw that mastodon uses. Get the cw that mastodon uses.
""" """
def get_cw(entry) do def get_cw(entry) do
with scope when not is_nil(scope) <- string_from_xpath("//mastodon:scope", entry), with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw cw
else _e -> nil else _e -> nil
end end

View file

@ -28,6 +28,13 @@ def user_fetcher(username) do
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true} plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
end 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 pipeline :well_known do
plug :accepts, ["xml", "xrd+xml"] plug :accepts, ["xml", "xrd+xml"]
end end
@ -51,6 +58,18 @@ def user_fetcher(username) do
get "/emoji", UtilController, :emoji get "/emoji", UtilController, :emoji
end 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 scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize get "/authorize", OAuthController, :authorize
post "/authorize", OAuthController, :create_authorization post "/authorize", OAuthController, :create_authorization
@ -101,6 +120,7 @@ def user_fetcher(username) do
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :api pipe_through :api
get "/instance", MastodonAPIController, :masto_instance get "/instance", MastodonAPIController, :masto_instance
get "/instance/peers", MastodonAPIController, :peers
post "/apps", MastodonAPIController, :create_app post "/apps", MastodonAPIController, :create_app
get "/custom_emojis", MastodonAPIController, :custom_emojis 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 "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/users/show", TwitterAPI.Controller, :show_user 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 "/statuses/show/:id", TwitterAPI.Controller, :fetch_status
get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation 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 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 "/friends/ids", TwitterAPI.Controller, :friends_ids
get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array
@ -243,6 +263,14 @@ def user_fetcher(username) do
delete "/auth/sign_out", MastodonAPIController, :logout delete "/auth/sign_out", MastodonAPIController, :logout
end 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 scope "/", Fallback do
get "/*path", RedirectController, :redirector get "/*path", RedirectController, :redirector
end end

View file

@ -1,3 +1,5 @@
<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> <h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<%= label f, :name, "Name" %> <%= label f, :name, "Name" %>

View file

@ -0,0 +1,11 @@
<%= 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 %>

View file

@ -0,0 +1,14 @@
<%= 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 %>

View file

@ -0,0 +1,6 @@
<%= if @error do %>
<p>Error following account</p>
<% else %>
<h2>Account followed!</h2>
<% end %>

View file

@ -0,0 +1,10 @@
<%= 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 %>

View file

@ -1,8 +1,12 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
require Logger
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
alias Comeonin.Pbkdf2
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User} alias Pleroma.{Repo, PasswordResetToken, User}
def show_password_reset(conn, %{"token" => token}) do def show_password_reset(conn, %{"token" => token}) do
@ -29,6 +33,72 @@ def help_test(conn, _params) do
json(conn, "ok") json(conn, "ok")
end 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) @instance Application.get_env(:pleroma, :instance)
def config(conn, _params) do def config(conn, _params) do
case get_format(conn) do case get_format(conn) do
@ -51,7 +121,7 @@ def config(conn, _params) do
site: %{ site: %{
name: Keyword.get(@instance, :name), name: Keyword.get(@instance, :name),
server: Web.base_url, 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") 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 def emoji(conn, _params) do
json conn, Enum.into(Formatter.get_custom_emoji(), %{}) json conn, Enum.into(Formatter.get_custom_emoji(), %{})
end 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 end

View file

@ -6,7 +6,7 @@ def to_map(%Object{} = object, _opts) do
data = object.data data = object.data
url = List.first(data["url"]) url = List.first(data["url"])
%{ %{
url: url["href"], url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"], mimetype: url["mediaType"],
id: data["uuid"], id: data["uuid"],
oembed: false oembed: false

View file

@ -316,10 +316,12 @@ def conversation_id_to_context(id) do
def get_external_profile(for_user, uri) do def get_external_profile(for_user, uri) do
with {:ok, %User{} = user} <- OStatus.find_or_make_user(uri) do with {:ok, %User{} = user} <- OStatus.find_or_make_user(uri) do
spawn(fn ->
with url <- user.info["topic"], with url <- user.info["topic"],
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
OStatus.handle_incoming(body) OStatus.handle_incoming(body)
end end
end)
{:ok, UserView.render("show.json", %{user: user, for: for_user})} {:ok, UserView.render("show.json", %{user: user, for: for_user})}
else _e -> else _e ->
{:error, "Couldn't find user"} {:error, "Couldn't find user"}

View file

@ -263,16 +263,18 @@ def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" =>
end end
end end
def followers(%{assigns: %{user: user}} = conn, _params) do def followers(conn, params) do
with {:ok, followers} <- User.get_followers(user) 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}) render(conn, UserView, "index.json", %{users: followers, for: user})
else else
_e -> bad_request_reply(conn, "Can't get followers") _e -> bad_request_reply(conn, "Can't get followers")
end end
end end
def friends(%{assigns: %{user: user}} = conn, _params) do def friends(conn, params) do
with {:ok, friends} <- User.get_friends(user) 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}) render(conn, UserView, "index.json", %{users: friends, for: user})
else else
_e -> bad_request_reply(conn, "Can't get friends") _e -> bad_request_reply(conn, "Can't get friends")

View file

@ -2,6 +2,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("show.json", %{user: user = %User{}} = assigns) do def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns) render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
@ -12,7 +13,7 @@ def render("index.json", %{users: users, for: user}) do
end end
def render("user.json", %{user: user = %User{}} = assigns) do 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 {following, follows_you, statusnet_blocking} = if assigns[:for] do
{ {
User.following?(assigns[:for], user), User.following?(assigns[:for], user),
@ -44,8 +45,9 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"screen_name" => user.nickname, "screen_name" => user.nickname,
"statuses_count" => user_info[:note_count], "statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => image_url(user.info["banner"]), "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) "background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
"is_local" => user.local
} }
if assigns[:token] do if assigns[:token] do

View file

@ -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: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}, {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}}, {: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 |> 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) 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) subject = XML.string_from_xpath("//Subject", doc)
salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, 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 = %{ data = %{
"magic_key" => magic_key, "magic_key" => magic_key,
"topic" => topic, "topic" => topic,
"subject" => subject, "subject" => subject,
"salmon" => salmon "salmon" => salmon,
"subscribe_address" => subscribe_address
} }
{:ok, data} {:ok, data}
end end

BIN
priv/static/emoji/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

BIN
priv/static/emoji/f_00b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
priv/static/emoji/f_00h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
priv/static/emoji/f_00t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
priv/static/emoji/f_01b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
priv/static/emoji/f_03b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
priv/static/emoji/f_10b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
priv/static/emoji/f_11b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
priv/static/emoji/f_11h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
priv/static/emoji/f_11t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
priv/static/emoji/f_12b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
priv/static/emoji/f_21b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
priv/static/emoji/f_22b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
priv/static/emoji/f_22h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
priv/static/emoji/f_22t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
priv/static/emoji/f_23b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
priv/static/emoji/f_30b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
priv/static/emoji/f_32b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
priv/static/emoji/f_33b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
priv/static/emoji/f_33h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
priv/static/emoji/f_33t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
priv/static/images/avi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.67f64792f89a96e59442c437c7ded0b3.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.ee87253244897e08bdce.js></script><script type=text/javascript src=/static/js/vendor.50cd70f77f559bfe1f27.js></script><script type=text/javascript src=/static/js/app.fefccf252cac9e1310ea.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b3deb1dd44970d86cc6b368f36fd09d9.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.15dfe939c498cca9840c.js></script><script type=text/javascript src=/static/js/vendor.409059e5a814f448f5bc.js></script><script type=text/javascript src=/static/js/app.30c01d7540d43b760f03.js></script></body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,7 +1,7 @@
{ {
"name": "Pleroma FE",
"theme": "pleroma-dark", "theme": "pleroma-dark",
"background": "/static/bg.jpg", "background": "/static/bg.jpg",
"logo": "/static/logo.png", "logo": "/static/logo.png",
"registrationOpen": true "defaultPath": "/main/all",
"chatDisabled": false
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var r=window.webpackJsonp;window.webpackJsonp=function(o,c){for(var p,l,s=0,i=[];s<o.length;s++)l=o[s],a[l]&&i.push.apply(i,a[l]),a[l]=0;for(p in c)Object.prototype.hasOwnProperty.call(c,p)&&(e[p]=c[p]);for(r&&r(o,c);i.length;)i.shift().call(null,t);if(c[0])return n[0]=0,t(0)};var n={},a={0:0};t.e=function(e,r){if(0===a[e])return r.call(null,t);if(void 0!==a[e])a[e].push(r);else{a[e]=[r];var n=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"409059e5a814f448f5bc",2:"30c01d7540d43b760f03"}[e]+".js",n.appendChild(o)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.15dfe939c498cca9840c.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
!function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={exports:{},id:n,loaded:!1};return e[n].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var f,p,s=0,l=[];s<c.length;s++)p=c[s],a[p]&&l.push.apply(l,a[p]),a[p]=0;for(f in o)e[f]=o[f];for(n&&n(c,o);l.length;)l.shift().call(null,t);if(o[0])return r[0]=0,t(0)};var r={},a={0:0};t.e=function(e,n){if(0===a[e])return n.call(null,t);if(void 0!==a[e])a[e].push(n);else{a[e]=[n];var r=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"50cd70f77f559bfe1f27",2:"fefccf252cac9e1310ea"}[e]+".js",r.appendChild(c)}},t.m=e,t.c=r,t.p="/"}([]);
//# sourceMappingURL=manifest.ee87253244897e08bdce.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -14,9 +14,9 @@ test "notifies someone when they are directly addressed" do
{:ok, [notification, other_notification]} = Notification.create_notifications(activity) {:ok, [notification, other_notification]} = Notification.create_notifications(activity)
assert notification.user_id == other_user.id notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id assert notification.activity_id == activity.id
assert other_notification.user_id == third_user.id
assert other_notification.activity_id == activity.id assert other_notification.activity_id == activity.id
end end
end end

View file

@ -19,10 +19,10 @@ test "Represent a user account" do
statuses_count: 5, statuses_count: 5,
note: user.bio, note: user.bio,
url: user.ap_id, url: user.ap_id,
avatar: "https://placehold.it/48x48", avatar: "http://localhost:4001/images/avi.png",
avatar_static: "https://placehold.it/48x48", avatar_static: "http://localhost:4001/images/avi.png",
header: "https://placehold.it/700x335", header: "http://localhost:4001/images/banner.png",
header_static: "https://placehold.it/700x335", header_static: "http://localhost:4001/images/banner.png",
source: %{ source: %{
note: "", note: "",
privacy: "public", privacy: "public",

View file

@ -35,7 +35,7 @@ test "the public timeline", %{conn: conn} do
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") {:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = conn conn = conn
|> get("/api/v1/timelines/public") |> get("/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, 200)) == 2 assert length(json_response(conn, 200)) == 2
@ -43,6 +43,11 @@ test "the public timeline", %{conn: conn} do
|> get("/api/v1/timelines/public", %{"local" => "True"}) |> get("/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, 200) assert [%{"content" => "test"}] = json_response(conn, 200)
conn = build_conn()
|> get("/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, 200)
end end
test "posting a status", %{conn: conn} do test "posting a status", %{conn: conn} do
@ -50,9 +55,9 @@ test "posting a status", %{conn: conn} do
conn = conn conn = conn
|> assign(:user, user) |> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "cofe", "spoiler_text" => "2hu"}) |> post("/api/v1/statuses", %{"status" => "cofe", "spoiler_text" => "2hu", "sensitive" => "false"})
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu"} = json_response(conn, 200) assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = json_response(conn, 200)
assert Repo.get(Activity, id) assert Repo.get(Activity, id)
end end
@ -145,7 +150,7 @@ test "list of notifications", %{conn: conn} do
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
expected_response = "hi <a href=\"#{user.ap_id}\">@#{user.nickname}</a>" expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200) assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -161,7 +166,7 @@ test "getting a single notification", %{conn: conn} do
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/notifications/#{notification.id}") |> get("/api/v1/notifications/#{notification.id}")
expected_response = "hi <a href=\"#{user.ap_id}\">@#{user.nickname}</a>" expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200) assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -581,11 +586,14 @@ test "get instance information" do
{:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"}) {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
Pleroma.Stats.update_stats()
conn = conn conn = conn
|> get("/api/v1/instance") |> get("/api/v1/instance")
assert result = json_response(conn, 200) assert result = json_response(conn, 200)
assert result["stats"]["user_count"] == 2 assert result["stats"]["user_count"] == 2
assert result["stats"]["status_count"] == 1
end end
end end

View file

@ -56,7 +56,9 @@ test "a note activity" do
test "contains mentions" do test "contains mentions" do
incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
user = insert(:user, %{ap_id: "https://pleroma.soykaf.com/users/lain"}) # a user with this ap id might be in the cache.
recipient = "https://pleroma.soykaf.com/users/lain"
user = User.get_cached_by_ap_id(recipient) || insert(:user, %{ap_id: recipient})
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)

View file

@ -26,6 +26,7 @@ test "returns a feed of the last 20 items of the user" do
<id>#{OStatus.feed_path(user)}</id> <id>#{OStatus.feed_path(user)}</id>
<title>#{user.nickname}'s timeline</title> <title>#{user.nickname}'s timeline</title>
<updated>#{most_recent_update}</updated> <updated>#{most_recent_update}</updated>
<logo>#{User.avatar_url(user)}</logo>
<link rel="hub" href="#{OStatus.pubsub_path(user)}" /> <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
<link rel="salmon" href="#{OStatus.salmon_path(user)}" /> <link rel="salmon" href="#{OStatus.salmon_path(user)}" />
<link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" /> <link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />

View file

@ -302,7 +302,8 @@ test "it returns user info in a hash" do
"host" => "social.heldscal.la", "host" => "social.heldscal.la",
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]} "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}"
} }
assert data == expected assert data == expected
end end
@ -325,7 +326,8 @@ test "it works with the uri" do
"host" => "social.heldscal.la", "host" => "social.heldscal.la",
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]} "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}"
} }
assert data == expected assert data == expected
end end

View file

@ -21,6 +21,7 @@ test "returns a user with id, uri, name and link" do
<summary>#{user.bio}</summary> <summary>#{user.bio}</summary>
<name>#{user.nickname}</name> <name>#{user.nickname}</name>
<link rel="avatar" href="#{User.avatar_url(user)}" /> <link rel="avatar" href="#{User.avatar_url(user)}" />
<link rel="header" href="#{User.banner_url(user)}" />
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)

View file

@ -518,7 +518,7 @@ test "it returns a user's followers", %{conn: conn} do
end end
describe "GET /api/statuses/friends" do describe "GET /api/statuses/friends" do
test "it returns a user's friends", %{conn: conn} do test "it returns the logged in user's friends", %{conn: conn} do
user = insert(:user) user = insert(:user)
followed_one = insert(:user) followed_one = insert(:user)
followed_two = insert(:user) followed_two = insert(:user)
@ -533,6 +533,36 @@ test "it returns a user's friends", %{conn: conn} do
assert MapSet.equal?(MapSet.new(json_response(conn, 200)), MapSet.new(UserView.render("index.json", %{users: [followed_one, followed_two], for: user}))) assert MapSet.equal?(MapSet.new(json_response(conn, 200)), MapSet.new(UserView.render("index.json", %{users: [followed_one, followed_two], for: user})))
end end
test "it returns a given user's friends with user_id", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn = conn
|> get("/api/statuses/friends", %{"user_id" => user.id})
assert MapSet.equal?(MapSet.new(json_response(conn, 200)), MapSet.new(UserView.render("index.json", %{users: [followed_one, followed_two], for: user})))
end
test "it returns a given user's friends with screen_name", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn = conn
|> get("/api/statuses/friends", %{"screen_name" => user.nickname})
assert MapSet.equal?(MapSet.new(json_response(conn, 200)), MapSet.new(UserView.render("index.json", %{users: [followed_one, followed_two], for: user})))
end
end end
describe "GET /friends/ids" do describe "GET /friends/ids" do

View file

@ -34,7 +34,7 @@ test "create a status" do
{ :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input) { :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. #2hu #epic #phantasmagoric<br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>" assert get_in(activity.data, ["object", "content"]) == "Hello again, <span><a href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. #2hu #epic #phantasmagoric<br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
assert get_in(activity.data, ["object", "type"]) == "Note" assert get_in(activity.data, ["object", "type"]) == "Note"
assert get_in(activity.data, ["object", "actor"]) == user.ap_id assert get_in(activity.data, ["object", "actor"]) == user.ap_id
assert get_in(activity.data, ["actor"]) == user.ap_id assert get_in(activity.data, ["actor"]) == user.ap_id
@ -291,7 +291,7 @@ test "it adds user links to an existing text" do
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
mentions = Pleroma.Formatter.parse_mentions(text) mentions = Pleroma.Formatter.parse_mentions(text)
expected_text = "<a href='#{gsimg.ap_id}'>@gsimg</a> According to <a href='#{archaeme.ap_id}'>@archaeme</a>, that is @daggsy. Also hello <a href='#{archaeme_remote.ap_id}'>@archaeme</a>" expected_text = "<span><a href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a href='#{archaeme.ap_id}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
assert Utils.add_user_links(text, mentions) == expected_text assert Utils.add_user_links(text, mentions) == expected_text
end end
@ -404,7 +404,7 @@ test "fetches a user by uri" do
assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"] assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"]
# Also fetches the feed. # Also fetches the feed.
assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status") # assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
end end
end end
end end

Some files were not shown because too many files have changed in this diff Show more