Merge branch 'develop' into 'patch-2'

# Conflicts:
#   mix.exs
This commit is contained in:
kaniini 2018-08-16 15:23:04 +00:00
commit f2fa09c50f
278 changed files with 997 additions and 191 deletions

View file

@ -13,6 +13,21 @@ Instead, overload the settings by editing the following files:
* `dev.secret.exs`: custom additional configuration for `MIX_ENV=dev`
* `prod.secret.exs`: custom additional configuration for `MIX_ENV=prod`
## Uploads configuration
To configure where to upload files, and wether or not
you want to remove automatically EXIF data from pictures
being uploaded.
config :pleroma, Pleroma.Upload,
uploads: "uploads",
strip_exif: false
* `uploads`: where to put the uploaded files, relative to pleroma's main directory.
* `strip_exif`: whether or not to remove EXIF data from uploaded pics automatically.
This needs Imagemagick installed on the system ( apt install imagemagick ).
## Block functionality
config :pleroma, :activitypub,

View file

@ -10,7 +10,11 @@
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
config :pleroma, Pleroma.Upload, uploads: "uploads"
config :pleroma, Pleroma.Upload,
uploads: "uploads",
strip_exif: false
config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
@ -50,6 +54,7 @@
version: version,
name: "Pleroma",
email: "example@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5000,
upload_limit: 16_000_000,
registrations_open: true,
@ -58,6 +63,19 @@
public: true,
quarantined_instances: []
config :pleroma, :fe,
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
show_who_to_follow_panel: false,
who_to_follow_provider:
"https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-osa-api.cgi?{{host}}+{{user}}",
who_to_follow_link: "https://vinayaka.distsn.org/?{{host}}+{{user}}",
scope_options_enabled: false
config :pleroma, :activitypub,
accept_blocks: true,
unfollow_blocked: true,
@ -93,6 +111,13 @@
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, :suggestions,
enabled: false,
third_party_engine:
"http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
timeout: 300_000,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.GenerateInviteToken do
use Mix.Task
@shortdoc "Generate invite token for user"
def run([]) do
Mix.Task.run("app.start")
with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
IO.puts("Generated user invite token")
IO.puts(
"Url: #{
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
token.token
)
}"
)
else
_ ->
IO.puts("Error creating token")
end
end
end

View file

@ -78,4 +78,8 @@ def get_create_activity_by_object_ap_id(ap_id) when is_binary(ap_id) do
end
def get_create_activity_by_object_ap_id(_), do: nil
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
def normalize(_), do: nil
end

View file

@ -116,7 +116,28 @@ def parse_mentions(text) do
_ -> []
end)
@emoji @finmoji_with_filenames ++ @emoji_from_file
@emoji_from_globs (
static_path = Path.join(:code.priv_dir(:pleroma), "static")
globs =
Application.get_env(:pleroma, :emoji, [])
|> Keyword.get(:shortcode_globs, [])
paths =
Enum.map(globs, fn glob ->
Path.join(static_path, glob)
|> Path.wildcard()
end)
|> Enum.concat()
Enum.map(paths, fn path ->
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path}
end)
)
@emoji @finmoji_with_filenames ++ @emoji_from_globs ++ @emoji_from_file
def emojify(text, emoji \\ @emoji)
def emojify(text, nil), do: text
@ -223,8 +244,8 @@ def add_hashtag_links({subs, text}, tags) do
subs =
subs ++
Enum.map(tags, fn {_, tag, uuid} ->
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
Enum.map(tags, fn {tag_text, tag, uuid} ->
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
{uuid, url}
end)

View file

@ -54,7 +54,7 @@ def info(text) do
String.split(text, "\r")
|> Enum.map(fn text ->
"i#{text}\tfake\(NULL)\t0\r\n"
"i#{text}\tfake\t(NULL)\t0\r\n"
end)
|> Enum.join("")
end
@ -77,14 +77,14 @@ def render_activities(activities) do
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"\r\n" <>
"i\tfake\t(NULL)\t0\r\n" <>
info(
HtmlSanitizeEx.strip_tags(
String.replace(activity.data["object"]["content"], "<br>", "\r")
)
)
end)
|> Enum.join("\r\n")
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
def response("") do

View file

@ -27,6 +27,10 @@ def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil
def get_cached_by_ap_id(ap_id) do
if Mix.env() == :test do
get_by_ap_id(ap_id)

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Web.Plugs.DigestPlug do
alias Plug.Conn
require Logger
def read_body(conn, opts) do
{:ok, body, conn} = Conn.read_body(conn, opts)
digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
{:ok, body, Conn.assign(conn, :digest, digest)}
end
end

View file

@ -19,6 +19,8 @@ def call(conn, _opts) do
cond do
signature && String.contains?(signature, user) ->
# set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed
conn =
conn
|> put_req_header(
@ -26,6 +28,14 @@ def call(conn, _opts) do
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
conn =
if conn.assigns[:digest] do
conn
|> put_req_header("digest", conn.assigns[:digest])
else
conn
end
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
signature ->

View file

@ -18,8 +18,10 @@ def store(%Plug.Upload{} = file, should_dedupe) do
File.cp!(file.path, result_file)
end
strip_exif_data(content_type, result_file)
%{
"type" => "Image",
"type" => "Document",
"url" => [
%{
"type" => "Link",
@ -67,6 +69,8 @@ def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
File.rename(uuidpath, result_file)
end
strip_exif_data(content_type, result_file)
%{
"type" => "Image",
"url" => [
@ -80,6 +84,16 @@ def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
}
end
def strip_exif_data(content_type, file) do
settings = Application.get_env(:pleroma, Pleroma.Upload)
do_strip = Keyword.fetch!(settings, :strip_exif)
[filetype, ext] = String.split(content_type, "/")
if filetype == "image" and do_strip == true do
Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
end
end
def upload_path do
settings = Application.get_env(:pleroma, Pleroma.Upload)
Keyword.fetch!(settings, :uploads)
@ -110,20 +124,20 @@ defp get_name(file, uuid, type, should_dedupe) do
if should_dedupe do
create_name(uuid, List.last(String.split(file.filename, ".")), type)
else
unless String.contains?(file.filename, ".") do
case type do
"image/png" -> file.filename <> ".png"
"image/jpeg" -> file.filename <> ".jpg"
"image/gif" -> file.filename <> ".gif"
"video/webm" -> file.filename <> ".webm"
"video/mp4" -> file.filename <> ".mp4"
"audio/mpeg" -> file.filename <> ".mp3"
"audio/ogg" -> file.filename <> ".ogg"
"audio/wav" -> file.filename <> ".wav"
_ -> file.filename
parts = String.split(file.filename, ".")
new_filename =
if length(parts) > 1 do
Enum.drop(parts, -1) |> Enum.join(".")
else
Enum.join(parts)
end
else
file.filename
case type do
"application/octet-stream" -> file.filename
"audio/mpeg" -> new_filename <> ".mp3"
"image/jpeg" -> new_filename <> ".jpg"
_ -> Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
end
end
end

View file

@ -398,6 +398,7 @@ def get_follow_requests(%User{} = user) do
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users}
end
@ -607,7 +608,7 @@ def delete(%User{} = user) do
|> Enum.each(fn activity ->
case activity.data["type"] do
"Create" ->
ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
ActivityPub.delete(Object.normalize(activity.data["object"]))
# TODO: Do something with likes, follows, repeats.
_ ->

View file

@ -0,0 +1,40 @@
defmodule Pleroma.UserInviteToken do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.{User, UserInviteToken, Repo}
schema "user_invite_tokens" do
field(:token, :string)
field(:used, :boolean, default: false)
timestamps()
end
def create_token do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
token = %UserInviteToken{
used: false,
token: token
}
Repo.insert(token)
end
def used_changeset(struct) do
struct
|> cast(%{}, [])
|> put_change(:used, true)
end
def mark_as_used(token) do
with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}
else
_e -> {:error, token}
end
end
end

View file

@ -30,7 +30,7 @@ defp check_actor_is_active(actor) do
end
def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.get_by_ap_id(map["id"]),
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map),
:ok <- check_actor_is_active(map["actor"]),
{:ok, map} <- MRF.filter(map),
@ -641,13 +641,23 @@ def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)})
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest
})
@httpoison.post(
inbox,
json,
[{"Content-Type", "application/activity+json"}, {"signature", signature}],
[
{"Content-Type", "application/activity+json"},
{"signature", signature},
{"digest", digest}
],
hackney: [pool: :default]
)
end
@ -670,7 +680,7 @@ def fetch_object_from_id(id) do
recv_timeout: 20000
),
{:ok, data} <- Jason.decode(body),
nil <- Object.get_by_ap_id(data["id"]),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
@ -679,7 +689,7 @@ def fetch_object_from_id(id) do
"object" => data
},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
{:ok, Object.normalize(activity.data["object"])}
else
object = %Object{} ->
{:ok, object}
@ -688,7 +698,7 @@ def fetch_object_from_id(id) do
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
{:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"])}
e -> e
end
end

View file

@ -13,18 +13,58 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
Enum.at(actor, 0)
end
def get_actor(%{"actor" => actor}) when is_map(actor) do
actor["id"]
end
def get_actor(%{"actor" => actor_list}) do
Enum.find(actor_list, fn %{"type" => type} -> type == "Person" end)
|> Map.get("id")
end
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
object
|> Map.put("actor", object["attributedTo"])
|> fix_actor
|> fix_attachments
|> fix_context
|> fix_in_reply_to
|> fix_emoji
|> fix_tag
|> fix_content_map
|> fix_addressing
end
def fix_addressing_list(map, field) do
if is_binary(map[field]) do
map
|> Map.put(field, [map[field]])
else
map
end
end
def fix_addressing(map) do
map
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
end
def fix_actor(%{"attributedTo" => actor} = object) do
object
|> Map.put("actor", get_actor(%{"actor" => actor}))
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
@ -122,7 +162,14 @@ def fix_content_map(object), do: object
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note"] do
actor = get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
@ -412,7 +459,7 @@ def handle_incoming(
def handle_incoming(_), do: :error
def get_obj_helper(id) do
if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
@ -460,14 +507,7 @@ def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = obj
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do
follow_activity_id =
if is_binary(data["object"]) do
data["object"]
else
data["object"]["id"]
end
with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
@ -485,14 +525,7 @@ def prepare_outgoing(%{"type" => "Accept"} = data) do
end
def prepare_outgoing(%{"type" => "Reject"} = data) do
follow_activity_id =
if is_binary(data["object"]) do
data["object"]
else
data["object"]["id"]
end
with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],

View file

@ -128,7 +128,7 @@ def lazy_put_object_defaults(map, activity \\ %{}) do
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Note"] do
when is_map(object_data) and type in ["Article", "Note"] do
with {:ok, _} <- Object.create(object_data) do
:ok
end

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
%{
@ -98,9 +98,6 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
info = User.user_info(user)
params = %{
"type" => ["Create", "Announce"],
"actor_id" => user.ap_id,
"whole_db" => true,
"limit" => "10"
}
@ -111,10 +108,8 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
params
end
activities = ActivityPub.fetch_public_activities(params)
min_id = Enum.at(activities, 0).id
activities = Enum.reverse(activities)
activities = ActivityPub.fetch_user_activities(user, nil, params)
min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id
collection =

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.get_by_ap_id(object_id),
%Object{} = object <- Object.normalize(object_id),
true <- user.info["is_moderator"] || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
@ -16,7 +16,7 @@ def delete(activity_id, user) do
def repeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.get_by_ap_id(activity.data["object"]["id"]) do
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.announce(user, object)
else
_ ->
@ -26,7 +26,7 @@ def repeat(id_or_ap_id, user) do
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.get_by_ap_id(activity.data["object"]["id"]) do
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.unannounce(user, object)
else
_ ->
@ -37,7 +37,7 @@ def unrepeat(id_or_ap_id, user) do
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
false <- activity.data["actor"] == user.ap_id,
object <- Object.get_by_ap_id(activity.data["object"]["id"]) do
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.like(user, object)
else
_ ->
@ -48,7 +48,7 @@ def favorite(id_or_ap_id, user) do
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
false <- activity.data["actor"] == user.ap_id,
object <- Object.get_by_ap_id(activity.data["object"]["id"]) do
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.unlike(user, object)
else
_ ->

View file

@ -35,7 +35,8 @@ defmodule Pleroma.Web.Endpoint do
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason,
length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit)
length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
plug(Plug.MethodOverride)

View file

@ -95,7 +95,7 @@ def handle(:incoming_ap_doc, params) do
params = Utils.normalize_params(params)
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.get_by_ap_id(params["id"]),
nil <- Activity.normalize(params["id"]),
{:ok, _activity} <- Transmogrifier.handle_incoming(params) do
else
%Activity{} ->

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification, Stats}
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub
@ -11,6 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
import Ecto.Query
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
action_fallback(:errors)
def create_app(conn, params) do
@ -125,7 +127,7 @@ def masto_instance(conn, _params) do
response = %{
uri: Web.base_url(),
title: Keyword.get(@instance, :name),
description: "A Pleroma instance, an alternative fediverse server",
description: Keyword.get(@instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email),
urls: %{
@ -428,16 +430,43 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) 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
def update_media(%{assigns: %{user: _}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]),
true <- is_binary(data["description"]),
description <- data["description"] do
new_data = %{object.data | "name" => description}
change = Object.change(object, %{data: new_data})
{:ok, media_obj} = Repo.update(change)
data =
object.data
new_data
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data})
end
end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
with {:ok, object} <- ActivityPub.upload(file) do
objdata =
if Map.has_key?(data, "description") do
Map.put(object.data, "name", data["description"])
else
object.data
end
change = Object.change(object, %{data: objdata})
{:ok, object} = Repo.update(change)
objdata =
objdata
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: objdata})
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)
@ -873,7 +902,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
},
compose: %{
me: "#{user.id}",
default_privacy: "public",
default_privacy: user.info["default_scope"] || "public",
default_sensitive: false
},
media_attachments: %{
@ -1070,4 +1099,38 @@ def errors(conn, _) do
|> put_status(500)
|> json("Something went wrong")
end
@suggestions Application.get_env(:pleroma, :suggestions)
def suggestions(%{assigns: %{user: user}} = conn, _) do
if Keyword.get(@suggestions, :enabled, false) do
api = Keyword.get(@suggestions, :third_party_engine, "")
timeout = Keyword.get(@suggestions, :timeout, 5000)
host =
Application.get_env(:pleroma, Pleroma.Web.Endpoint)
|> Keyword.get(:url)
|> Keyword.get(:host)
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
{:ok, data} <- Jason.decode(body) do
data2 =
Enum.slice(data, 0, 40)
|> Enum.map(fn x ->
Map.put(x, "id", User.get_or_fetch(x["acct"]).id)
end)
conn
|> json(data2)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
else
json(conn, [])
end
end
end

View file

@ -14,6 +14,18 @@ def render("account.json", %{user: user}) do
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
emojis =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
%{
"shortcode" => String.trim(name, ":"),
"url" => MediaProxy.url(url),
"static_url" => MediaProxy.url(url),
"visible_in_picker" => false
}
end)
%{
id: to_string(user.id),
username: hd(String.split(user.nickname, "@")),
@ -30,7 +42,7 @@ def render("account.json", %{user: user}) do
avatar_static: image,
header: header,
header_static: header,
emojis: [],
emojis: emojis,
fields: [],
source: %{
note: "",

View file

@ -54,8 +54,7 @@ def render(
%{
id: to_string(activity.id),
uri: object,
# TODO: This might be wrong, check with mastodon.
url: nil,
url: object,
account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
@ -128,7 +127,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
content: HtmlSanitizeEx.basic_html(object["content"]),
content: render_content(object),
created_at: created_at,
reblogs_count: announcement_count,
favourites_count: like_count,
@ -170,7 +169,8 @@ def render("attachment.json", %{attachment: attachment}) do
remote_url: href,
preview_url: MediaProxy.url(href),
text_url: href,
type: type
type: type,
description: attachment["name"]
}
end
@ -207,4 +207,21 @@ def get_visibility(object) do
"direct"
end
end
def render_content(%{"type" => "Article"} = object) do
summary = object["name"]
content =
if !!summary and summary != "" do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
end
HtmlSanitizeEx.basic_html(content)
end
def render_content(object) do
HtmlSanitizeEx.basic_html(object["content"])
end
end

View file

@ -4,8 +4,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats
alias Pleroma.Web
@instance Application.get_env(:pleroma, :instance)
def schemas(conn, _params) do
response = %{
links: [
@ -21,20 +19,23 @@ def schemas(conn, _params) do
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
def nodeinfo(conn, %{"version" => "2.0"}) do
instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
stats = Stats.get_stats()
response = %{
version: "2.0",
software: %{
name: "pleroma",
version: Keyword.get(@instance, :version)
version: Keyword.get(instance, :version)
},
protocols: ["ostatus", "activitypub"],
services: %{
inbound: [],
outbound: []
},
openRegistrations: Keyword.get(@instance, :registrations_open),
openRegistrations: Keyword.get(instance, :registrations_open),
usage: %{
users: %{
total: stats.user_count || 0
@ -42,7 +43,16 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
localPosts: stats.status_count || 0
},
metadata: %{
nodeName: Keyword.get(@instance, :name)
nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description),
mediaProxy: Keyword.get(media_proxy, :enabled),
private: !Keyword.get(instance, :public, true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""),
timeout: Keyword.get(suggestions, :timeout, 5000),
web: Keyword.get(suggestions, :web, "")
}
}
}

View file

@ -246,7 +246,7 @@ def to_simple_form(
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.recipients || []) |> get_mentions
follow_activity = Activity.get_by_ap_id(follow_activity["id"])
follow_activity = Activity.normalize(follow_activity)
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandler do
def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
object when not is_nil(object) <- Object.get_by_ap_id(id),
%Object{} = object <- Object.normalize(id),
{:ok, delete} <- ActivityPub.delete(object, false) do
delete
end

View file

@ -89,7 +89,7 @@ def handle_incoming(xml_string) do
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.get_by_ap_id(retweeted_activity.data["object"]["id"]),
%Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
@ -107,7 +107,7 @@ def handle_share(entry, doc) do
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.get_by_ap_id(favorited_activity.data["object"]["id"]),
%Object{} = object <- Object.normalize(favorited_activity.data["object"]),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator}
alias Pleroma.Web.XML
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ActivityPub
@ -90,7 +91,7 @@ def object(conn, %{"uuid" => uuid}) do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, activity, user)
_ -> represent_activity(conn, nil, activity, user)
end
else
{:public?, false} ->
@ -107,12 +108,12 @@ def object(conn, %{"uuid" => uuid}) do
def activity(conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(id)},
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
case format = get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, activity, user)
_ -> represent_activity(conn, format, activity, user)
end
else
{:public?, false} ->
@ -130,14 +131,14 @@ def notice(conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
case format = get_format(conn) do
"html" ->
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
_ ->
represent_activity(conn, activity, user)
represent_activity(conn, format, activity, user)
end
else
{:public?, false} ->
@ -151,7 +152,13 @@ def notice(conn, %{"id" => id}) do
end
end
defp represent_activity(conn, activity, user) do
defp represent_activity(conn, "activity+json", activity, user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity}))
end
defp represent_activity(conn, _, activity, user) do
response =
activity
|> ActivityRepresenter.to_simple_form(user, true)

View file

@ -127,6 +127,7 @@ def user_fetcher(username) do
get("/notifications/:id", MastodonAPIController, :get_notification)
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
@ -140,6 +141,8 @@ def user_fetcher(username) do
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/suggestions", MastodonAPIController, :suggestions)
end
scope "/api/web", Pleroma.Web.MastodonAPI do
@ -201,9 +204,7 @@ def user_fetcher(username) do
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
if @registrations_open do
post("/account/register", TwitterAPI.Controller, :register)
end
post("/account/register", TwitterAPI.Controller, :register)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
@ -355,6 +356,7 @@ def user_fetcher(username) do
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector)
end
end
@ -369,4 +371,8 @@ def redirector(conn, _params) do
|> send_file(200, "priv/static/index.html")
end
end
def registration_page(conn, params) do
redirector(conn, params)
end
end

View file

@ -158,7 +158,7 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []
parent = Object.get_by_ap_id(item.data["object"])
parent = Object.normalize(item.data["object"])
unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})

View file

@ -99,6 +99,10 @@ def do_remote_follow(conn, %{
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
_e ->
conn
|> render("follow_login.html", %{
@ -117,6 +121,11 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
e ->
Logger.debug("Remote follow failed with error #{inspect(e)}")
@ -126,6 +135,8 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
end
@instance Application.get_env(:pleroma, :instance)
@instance_fe Application.get_env(:pleroma, :fe)
@instance_chat Application.get_env(:pleroma, :chat)
def config(conn, _params) do
case get_format(conn) do
"xml" ->
@ -148,9 +159,24 @@ def config(conn, _params) do
json(conn, %{
site: %{
name: Keyword.get(@instance, :name),
description: Keyword.get(@instance, :description),
server: Web.base_url(),
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"),
private: if(Keyword.get(@instance, :public, true), do: "0", else: "1"),
pleromafe: %{
theme: Keyword.get(@instance_fe, :theme),
background: Keyword.get(@instance_fe, :background),
logo: Keyword.get(@instance_fe, :logo),
redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(@instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
showWhoToFollowPanel: Keyword.get(@instance_fe, :show_who_to_follow_panel),
scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
whoToFollowProvider: Keyword.get(@instance_fe, :who_to_follow_provider),
whoToFollowLink: Keyword.get(@instance_fe, :who_to_follow_link)
}
}
})
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.{Activity, User}
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView}
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView}
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Formatter
@ -164,14 +164,7 @@ def to_map(
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
summary = activity.data["object"]["summary"]
content =
if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
{summary, content} = ActivityView.render_content(object)
html =
HtmlSanitizeEx.basic_html(content)
@ -198,7 +191,8 @@ def to_map(
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
"summary" => object["summary"]
}
end

View file

@ -1,11 +1,13 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{User, Activity, Repo, Object}
alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI}
import Ecto.Query
@instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison)
@registrations_open Keyword.get(@instance, :registrations_open)
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
@ -120,6 +122,8 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
end
def register_user(params) do
tokenString = params["token"]
params = %{
nickname: params["nickname"],
name: params["fullname"],
@ -129,17 +133,33 @@ def register_user(params) do
password_confirmation: params["confirm"]
}
changeset = User.register_changeset(%User{}, params)
# no need to query DB if registration is open
token =
unless @registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
with {:ok, user} <- Repo.insert(changeset) do
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
cond do
@registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params)
{:error, %{error: errors}}
with {:ok, user} <- Repo.insert(changeset) do
!@registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
!@registrations_open && is_nil(token) ->
{:error, "Invalid token"}
!@registrations_open && token.used ->
{:error, "Expired token"}
end
end

View file

@ -431,6 +431,19 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
user
end
user =
if default_scope = params["default_scope"] do
with new_info <- Map.put(user.info, "default_scope", default_scope),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)

View file

@ -228,15 +228,7 @@ def render(
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
summary = activity.data["object"]["summary"]
content = object["content"]
content =
if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
{summary, content} = render_content(object)
html =
HtmlSanitizeEx.basic_html(content)
@ -263,7 +255,41 @@ def render(
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
"summary" => summary
}
end
def render_content(%{"type" => "Note"} = object) do
summary = object["summary"]
content =
if !!summary and summary != "" do
"<p>#{summary}</p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(%{"type" => "Article"} = object) do
summary = object["name"] || object["summary"]
content =
if !!summary and summary != "" do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(object) do
summary = object["summary"] || "Unhandled activity type: #{object["type"]}"
content = "<p>#{summary}</p>#{object["content"]}"
{summary, content}
end
end

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
alias Pleroma.User
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
@ -28,9 +29,19 @@ def render("user.json", %{user: user = %User{}} = assigns) do
user_info = User.get_cached_user_info(user)
emoji =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
bio = HtmlSanitizeEx.strip_tags(user.bio)
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HtmlSanitizeEx.strip_tags(user.bio),
"description" => bio,
"description_html" => bio |> Formatter.emojify(emoji),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
@ -39,6 +50,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name,
"name_html" => HtmlSanitizeEx.strip_tags(user.name) |> Formatter.emojify(emoji),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
@ -52,7 +64,8 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
"is_local" => user.local,
"locked" => !!user.info["locked"]
"locked" => !!user.info["locked"],
"default_scope" => user.info["default_scope"] || "public"
}
if assigns[:token] do

View file

@ -45,6 +45,7 @@ defp deps do
{:cachex, "~> 3.0.2"},
{:httpoison, "~> 1.2.0"},
{:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"}
{:ex_machina, "~> 2.2", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test}

View file

@ -25,6 +25,7 @@
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},

View file

@ -0,0 +1,12 @@
defmodule Pleroma.Repo.Migrations.CreateUserInviteTokens do
use Ecto.Migration
def change do
create table(:user_invite_tokens) do
add :token, :string
add :used, :boolean, default: false
timestamps()
end
end
end

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=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.5d0189b6f119febde070b703869bbd06.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.f2341edd686e54ee9b4a.js></script><script type=text/javascript src=/static/js/vendor.a93310d51acbd9480094.js></script><script type=text/javascript src=/static/js/app.de965bb2a0a8bffbeafa.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=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.4e316dc76ab907cb78bb88b978ce04e2.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.212bbb8f66cdc9a7eebf.js></script><script type=text/javascript src=/static/js/vendor.da712c56a19114013b34.js></script><script type=text/javascript src=/static/js/app.9d52d5cd0ef719b7db59.js></script></body></html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,5 @@
CACHE MANIFEST
#ver:2018-4-9 21:57:37
#ver:2018-8-12 18:01:32
#plugin:4.8.4
CACHE:
@ -13,33 +13,46 @@ CACHE:
/packs/features/home_timeline.js
/packs/features/public_timeline.js
/packs/features/community_timeline.js
/packs/features/favourited_statuses.js
/packs/features/list_timeline.js
/packs/features/direct_timeline.js
/packs/features/pinned_statuses.js
/packs/features/domain_blocks.js
/packs/features/following.js
/packs/features/followers.js
/packs/features/favourited_statuses.js
/packs/features/list_timeline.js
/packs/features/account_gallery.js
/packs/features/hashtag_timeline.js
/packs/features/status.js
/packs/features/account_gallery.js
/packs/features/blocks.js
/packs/features/lists.js
/packs/modals/report_modal.js
/packs/features/getting_started.js
/packs/features/follow_requests.js
/packs/features/mutes.js
/packs/features/blocks.js
/packs/features/reblogs.js
/packs/features/favourites.js
/packs/features/getting_started.js
/packs/features/keyboard_shortcuts.js
/packs/modals/mute_modal.js
/packs/features/generic_not_found.js
/packs/features/list_editor.js
/packs/modals/embed_modal.js
/packs/status/media_gallery.js
/packs/containers/media_container.js
/packs/share.js
/packs/application.js
/packs/about.js
/packs/public.js
/packs/mailer.js
/packs/mastodon-light.js
/packs/contrast.js
/packs/default.js
/packs/public.js
/packs/admin.js
/packs/common.js
/packs/common.css
/packs/mailer.css
/packs/default.css
/packs/contrast.css
/packs/mastodon-light.css
/packs/manifest.json
NETWORK:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"contrast.css","sourceRoot":""}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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