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 1210 additions and 1715 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, ecto_repos: [Pleroma.Repo]
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 @@ config :pleroma, :instance,
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 @@ config :pleroma, :instance,
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 @@ config :pleroma, :gopher,
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 @@ defmodule Pleroma.Activity 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 @@ defmodule Pleroma.Formatter 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 @@ defmodule Pleroma.Formatter 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 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler 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 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler 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 @@ defmodule Pleroma.Object 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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 @@ defmodule Pleroma.Upload 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 @@ defmodule Pleroma.Upload do
File.rename(uuidpath, result_file)
end
strip_exif_data(content_type, result_file)
%{
"type" => "Image",
"url" => [
@ -80,6 +84,16 @@ defmodule Pleroma.Upload 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 @@ defmodule Pleroma.Upload 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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.UserView do
info = User.user_info(user)
params = %{
"type" => ["Create", "Announce"],
"actor_id" => user.ap_id,
"whole_db" => true,
"limit" => "10"
}
@ -111,10 +108,8 @@ defmodule Pleroma.Web.ActivityPub.UserView 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file) do
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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
},
compose: %{
me: "#{user.id}",
default_privacy: "public",
default_privacy: user.info["default_scope"] || "public",
default_sensitive: false
},
media_attachments: %{
@ -1070,4 +1099,38 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
avatar_static: image,
header: header,
header_static: header,
emojis: [],
emojis: emojis,
fields: [],
source: %{
note: "",

View File

@ -54,8 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{
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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
remote_url: href,
preview_url: MediaProxy.url(href),
text_url: href,
type: type
type: type,
description: attachment["name"]
}
end
@ -207,4 +207,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController 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 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.Router 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 @@ defmodule Pleroma.Web.Router 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 @@ defmodule Pleroma.Web.Router 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 @@ defmodule Pleroma.Web.Router do
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector)
end
end
@ -369,4 +371,8 @@ defmodule Fallback.RedirectController 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 @@ defmodule Pleroma.Web.Streamer do
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 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
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 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
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 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
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 @@ defmodule Pleroma.Web.TwitterAPI.UtilController 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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
"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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
def register_user(params) do
tokenString = params["token"]
params = %{
nickname: params["nickname"],
name: params["fullname"],
@ -129,17 +133,33 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
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 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
"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 @@ defmodule Pleroma.Web.TwitterAPI.UserView 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 @@ defmodule Pleroma.Web.TwitterAPI.UserView 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 @@ defmodule Pleroma.Web.TwitterAPI.UserView 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 @@ defmodule Pleroma.Mixfile 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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
webpackJsonp([69],{658:function(e,c,t){"use strict";function o(e){var c=e.detail,t=c[0],o=document.querySelector('[data-id="'+t.id+'"]');o&&o.parentNode.removeChild(o)}Object.defineProperty(c,"__esModule",{value:!0});var n=t(144);t.n(n);[].forEach.call(document.querySelectorAll(".trash-button"),function(e){e.addEventListener("ajax:success",o)});Object(n.delegate)(document,"#batch_checkbox_all","change",function(e){var c=e.target;[].forEach.call(document.querySelectorAll('.batch-checkbox input[type="checkbox"]'),function(e){e.checked=c.checked})}),Object(n.delegate)(document,'.batch-checkbox input[type="checkbox"]',"change",function(){var e=document.querySelector("#batch_checkbox_all");e&&(e.checked=[].every.call(document.querySelectorAll('.batch-checkbox input[type="checkbox"]'),function(e){return e.checked}))}),Object(n.delegate)(document,".media-spoiler-show-button","click",function(){[].forEach.call(document.querySelectorAll("button.media-spoiler"),function(e){e.click()})}),Object(n.delegate)(document,".media-spoiler-hide-button","click",function(){[].forEach.call(document.querySelectorAll(".spoiler-button.spoiler-button--visible button"),function(e){e.click()})})}},[658]);
webpackJsonp([85],{661:function(e,c,t){"use strict";function o(e){var c=e.detail,t=c[0],o=document.querySelector('[data-id="'+t.id+'"]');o&&o.parentNode.removeChild(o)}Object.defineProperty(c,"__esModule",{value:!0});var n=t(152);t.n(n);[].forEach.call(document.querySelectorAll(".trash-button"),function(e){e.addEventListener("ajax:success",o)});var l='.batch-checkbox input[type="checkbox"]';Object(n.delegate)(document,"#batch_checkbox_all","change",function(e){var c=e.target;[].forEach.call(document.querySelectorAll(l),function(e){e.checked=c.checked})}),Object(n.delegate)(document,l,"change",function(){var e=document.querySelector("#batch_checkbox_all");e&&(e.checked=[].every.call(document.querySelectorAll(l),function(e){return e.checked}),e.indeterminate=!e.checked&&[].some.call(document.querySelectorAll(l),function(e){return e.checked}))}),Object(n.delegate)(document,".media-spoiler-show-button","click",function(){[].forEach.call(document.querySelectorAll("button.media-spoiler"),function(e){e.click()})}),Object(n.delegate)(document,".media-spoiler-hide-button","click",function(){[].forEach.call(document.querySelectorAll(".spoiler-button.spoiler-button--visible button"),function(e){e.click()})})}},[661]);
//# sourceMappingURL=admin.js.map

File diff suppressed because one or more lines are too long

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:

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 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"contrast.css","sourceRoot":""}

View File

@ -0,0 +1,2 @@
webpackJsonp([82],{803:function(n,c){}},[803]);
//# sourceMappingURL=contrast.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///contrast.js"],"names":["webpackJsonp","803","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"contrast.js","sourcesContent":["webpackJsonp([82],{\n\n/***/ 803:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[803]);\n\n\n// WEBPACK FOOTER //\n// contrast.js"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
webpackJsonp([68],{799:function(n,c){}},[799]);
webpackJsonp([83],{802:function(n,c){}},[802]);
//# sourceMappingURL=default.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["webpack:///default.js"],"names":["webpackJsonp","799","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"default.js","sourcesContent":["webpackJsonp([68],{\n\n/***/ 799:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[799]);\n\n\n// WEBPACK FOOTER //\n// default.js"],"sourceRoot":""}
{"version":3,"sources":["webpack:///default.js"],"names":["webpackJsonp","802","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"default.js","sourcesContent":["webpackJsonp([83],{\n\n/***/ 802:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[802]);\n\n\n// WEBPACK FOOTER //\n// default.js"],"sourceRoot":""}

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

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

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

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

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
webpackJsonp([23],{150:function(n,e,i){"use strict";i.d(e,"a",function(){return v});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(0),f=i.n(d),h=i(10),p=i.n(h),v=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleClick=function(){t.props.onClick()},r=i,l()(t,r)}return s()(e,n),e.prototype.render=function(){var n=this.props,e=n.icon,i=n.type,t=n.active,o=n.columnHeaderId,a="";return e&&(a=r()("i",{className:"fa fa-fw fa-"+e+" column-header__icon"})),r()("h1",{className:p()("column-header",{active:t}),id:o||null},void 0,r()("button",{onClick:this.handleClick},void 0,a,i))},e}(f.a.PureComponent)},284:function(n,e,i){"use strict";i.d(e,"a",function(){return g});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(34),f=i.n(d),h=i(0),p=i.n(h),v=i(150),b=i(91),m=i(35),g=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleHeaderClick=function(){var n=t.node.querySelector(".scrollable");n&&(t._interruptScrollAnimation=Object(b.b)(n))},t.handleScroll=f()(function(){void 0!==t._interruptScrollAnimation&&t._interruptScrollAnimation()},200),t.setRef=function(n){t.node=n},r=i,l()(t,r)}return s()(e,n),e.prototype.scrollTop=function(){var n=this.node.querySelector(".scrollable");n&&(this._interruptScrollAnimation=Object(b.b)(n))},e.prototype.render=function(){var n=this.props,e=n.heading,i=n.icon,t=n.children,o=n.active,a=n.hideHeadingOnMobile,c=e&&(!a||a&&!Object(m.b)(window.innerWidth)),l=c&&e.replace(/ /g,"-"),u=c&&r()(v.a,{icon:i,active:o,type:e,onClick:this.handleHeaderClick,columnHeaderId:l});return p.a.createElement("div",{ref:this.setRef,role:"region","aria-labelledby":l,className:"column",onScroll:this.handleScroll},u,t)},e}(p.a.PureComponent)},820:function(n,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(284)),c=i(843),l=function(){return r()(a.a,{},void 0,r()(c.a,{}))};e.default=l},843:function(n,e,i){"use strict";var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(6)),c=function(){return r()("div",{className:"regeneration-indicator missing-indicator"},void 0,r()("div",{},void 0,r()("div",{className:"regeneration-indicator__label"},void 0,r()(a.b,{id:"missing_indicator.label",tagName:"strong",defaultMessage:"Not found"}),r()(a.b,{id:"missing_indicator.sublabel",defaultMessage:"This resource could not be found"}))))};e.a=c}});
webpackJsonp([30],{155:function(n,e,i){"use strict";i.d(e,"a",function(){return v});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(0),f=i.n(d),h=i(10),p=i.n(h),v=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleClick=function(){t.props.onClick()},r=i,l()(t,r)}return s()(e,n),e.prototype.render=function(){var n=this.props,e=n.icon,i=n.type,t=n.active,o=n.columnHeaderId,a="";return e&&(a=r()("i",{className:"fa fa-fw fa-"+e+" column-header__icon"})),r()("h1",{className:p()("column-header",{active:t}),id:o||null},void 0,r()("button",{onClick:this.handleClick},void 0,a,i))},e}(f.a.PureComponent)},274:function(n,e,i){"use strict";i.d(e,"a",function(){return g});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(32),f=i.n(d),h=i(0),p=i.n(h),v=i(155),m=i(91),b=i(43),g=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleHeaderClick=function(){var n=t.node.querySelector(".scrollable");n&&(t._interruptScrollAnimation=Object(m.b)(n))},t.handleScroll=f()(function(){void 0!==t._interruptScrollAnimation&&t._interruptScrollAnimation()},200),t.setRef=function(n){t.node=n},r=i,l()(t,r)}return s()(e,n),e.prototype.scrollTop=function(){var n=this.node.querySelector(".scrollable");n&&(this._interruptScrollAnimation=Object(m.b)(n))},e.prototype.render=function(){var n=this.props,e=n.heading,i=n.icon,t=n.children,o=n.active,a=n.hideHeadingOnMobile,c=e&&(!a||a&&!Object(b.b)(window.innerWidth)),l=c&&e.replace(/ /g,"-"),u=c&&r()(v.a,{icon:i,active:o,type:e,onClick:this.handleHeaderClick,columnHeaderId:l});return p.a.createElement("div",{ref:this.setRef,role:"region","aria-labelledby":l,className:"column",onScroll:this.handleScroll},u,t)},e}(p.a.PureComponent)},829:function(n,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(274)),c=i(866),l=function(){return r()(a.a,{},void 0,r()(c.a,{}))};e.default=l},866:function(n,e,i){"use strict";var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(7)),c=function(){return r()("div",{className:"regeneration-indicator missing-indicator"},void 0,r()("div",{},void 0,r()("div",{className:"regeneration-indicator__figure"}),r()("div",{className:"regeneration-indicator__label"},void 0,r()(a.b,{id:"missing_indicator.label",tagName:"strong",defaultMessage:"Not found"}),r()(a.b,{id:"missing_indicator.sublabel",defaultMessage:"This resource could not be found"}))))};e.a=c}});
//# sourceMappingURL=generic_not_found.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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