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 972 additions and 175 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` * `dev.secret.exs`: custom additional configuration for `MIX_ENV=dev`
* `prod.secret.exs`: custom additional configuration for `MIX_ENV=prod` * `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 ## Block functionality
config :pleroma, :activitypub, config :pleroma, :activitypub,

View file

@ -10,7 +10,11 @@
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes 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 # Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
@ -50,6 +54,7 @@
version: version, version: version,
name: "Pleroma", name: "Pleroma",
email: "example@example.com", email: "example@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5000, limit: 5000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
registrations_open: true, registrations_open: true,
@ -58,6 +63,19 @@
public: true, public: true,
quarantined_instances: [] 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, config :pleroma, :activitypub,
accept_blocks: true, accept_blocks: true,
unfollow_blocked: true, unfollow_blocked: true,
@ -93,6 +111,13 @@
ip: {0, 0, 0, 0}, ip: {0, 0, 0, 0},
port: 9999 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -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 end
def get_create_activity_by_object_ap_id(_), do: nil 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 end

View file

@ -116,7 +116,28 @@ def parse_mentions(text) do
_ -> [] _ -> []
end) 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, emoji \\ @emoji)
def emojify(text, nil), do: text def emojify(text, nil), do: text
@ -223,8 +244,8 @@ def add_hashtag_links({subs, text}, tags) do
subs = subs =
subs ++ subs ++
Enum.map(tags, fn {_, tag, uuid} -> Enum.map(tags, fn {tag_text, tag, uuid} ->
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>" url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
{uuid, url} {uuid, url}
end) end)

View file

@ -54,7 +54,7 @@ def info(text) do
String.split(text, "\r") String.split(text, "\r")
|> Enum.map(fn text -> |> Enum.map(fn text ->
"i#{text}\tfake\(NULL)\t0\r\n" "i#{text}\tfake\t(NULL)\t0\r\n"
end) end)
|> Enum.join("") |> Enum.join("")
end end
@ -77,14 +77,14 @@ def render_activities(activities) do
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <> info("#{like_count} likes, #{announcement_count} repeats") <>
"\r\n" <> "i\tfake\t(NULL)\t0\r\n" <>
info( info(
HtmlSanitizeEx.strip_tags( HtmlSanitizeEx.strip_tags(
String.replace(activity.data["object"]["content"], "<br>", "\r") String.replace(activity.data["object"]["content"], "<br>", "\r")
) )
) )
end) end)
|> Enum.join("\r\n") |> Enum.join("i\tfake\t(NULL)\t0\r\n")
end end
def response("") do 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))) Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end 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 def get_cached_by_ap_id(ap_id) do
if Mix.env() == :test do if Mix.env() == :test do
get_by_ap_id(ap_id) 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 cond do
signature && String.contains?(signature, user) -> 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 =
conn conn
|> put_req_header( |> put_req_header(
@ -26,6 +28,14 @@ def call(conn, _opts) do
String.downcase("#{conn.method}") <> " #{conn.request_path}" 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)) assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
signature -> signature ->

View file

@ -18,8 +18,10 @@ def store(%Plug.Upload{} = file, should_dedupe) do
File.cp!(file.path, result_file) File.cp!(file.path, result_file)
end end
strip_exif_data(content_type, result_file)
%{ %{
"type" => "Image", "type" => "Document",
"url" => [ "url" => [
%{ %{
"type" => "Link", "type" => "Link",
@ -67,6 +69,8 @@ def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
File.rename(uuidpath, result_file) File.rename(uuidpath, result_file)
end end
strip_exif_data(content_type, result_file)
%{ %{
"type" => "Image", "type" => "Image",
"url" => [ "url" => [
@ -80,6 +84,16 @@ def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
} }
end 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 def upload_path do
settings = Application.get_env(:pleroma, Pleroma.Upload) settings = Application.get_env(:pleroma, Pleroma.Upload)
Keyword.fetch!(settings, :uploads) Keyword.fetch!(settings, :uploads)
@ -110,20 +124,20 @@ defp get_name(file, uuid, type, should_dedupe) do
if should_dedupe do if should_dedupe do
create_name(uuid, List.last(String.split(file.filename, ".")), type) create_name(uuid, List.last(String.split(file.filename, ".")), type)
else else
unless String.contains?(file.filename, ".") do parts = String.split(file.filename, ".")
case type do
"image/png" -> file.filename <> ".png" new_filename =
"image/jpeg" -> file.filename <> ".jpg" if length(parts) > 1 do
"image/gif" -> file.filename <> ".gif" Enum.drop(parts, -1) |> Enum.join(".")
"video/webm" -> file.filename <> ".webm" else
"video/mp4" -> file.filename <> ".mp4" Enum.join(parts)
"audio/mpeg" -> file.filename <> ".mp3"
"audio/ogg" -> file.filename <> ".ogg"
"audio/wav" -> file.filename <> ".wav"
_ -> file.filename
end 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 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.map(reqs, fn req -> req.actor end)
|> Enum.uniq() |> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users} {:ok, users}
end end
@ -607,7 +608,7 @@ def delete(%User{} = user) do
|> Enum.each(fn activity -> |> Enum.each(fn activity ->
case activity.data["type"] do case activity.data["type"] do
"Create" -> "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. # 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 end
def insert(map, local \\ true) when is_map(map) do 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), map <- lazy_put_activity_defaults(map),
:ok <- check_actor_is_active(map["actor"]), :ok <- check_actor_is_active(map["actor"]),
{:ok, map} <- MRF.filter(map), {: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}") Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
signature = 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( @httpoison.post(
inbox, inbox,
json, json,
[{"Content-Type", "application/activity+json"}, {"signature", signature}], [
{"Content-Type", "application/activity+json"},
{"signature", signature},
{"digest", digest}
],
hackney: [pool: :default] hackney: [pool: :default]
) )
end end
@ -670,7 +680,7 @@ def fetch_object_from_id(id) do
recv_timeout: 20000 recv_timeout: 20000
), ),
{:ok, data} <- Jason.decode(body), {:ok, data} <- Jason.decode(body),
nil <- Object.get_by_ap_id(data["id"]), nil <- Object.normalize(data),
params <- %{ params <- %{
"type" => "Create", "type" => "Create",
"to" => data["to"], "to" => data["to"],
@ -679,7 +689,7 @@ def fetch_object_from_id(id) do
"object" => data "object" => data
}, },
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.get_by_ap_id(activity.data["object"]["id"])} {:ok, Object.normalize(activity.data["object"])}
else else
object = %Object{} -> object = %Object{} ->
{:ok, 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...") Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do 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 e -> e
end end
end end

View file

@ -13,18 +13,58 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
require Logger 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 """ @doc """
Modifies an incoming AP object (mastodon format) to our internal format. Modifies an incoming AP object (mastodon format) to our internal format.
""" """
def fix_object(object) do def fix_object(object) do
object object
|> Map.put("actor", object["attributedTo"]) |> fix_actor
|> fix_attachments |> fix_attachments
|> fix_context |> fix_context
|> fix_in_reply_to |> fix_in_reply_to
|> fix_emoji |> fix_emoji
|> fix_tag |> fix_tag
|> fix_content_map |> 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 end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) 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 # TODO: validate those with a Ecto scheme
# - tags # - tags
# - emoji # - 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"]), with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"]) object = fix_object(data["object"])
@ -412,7 +459,7 @@ def handle_incoming(
def handle_incoming(_), do: :error def handle_incoming(_), do: :error
def get_obj_helper(id) do 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 end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do 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, # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does. # because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do def prepare_outgoing(%{"type" => "Accept"} = data) do
follow_activity_id = with follow_activity <- Activity.normalize(data["object"]) do
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
object = %{ object = %{
"actor" => follow_activity.actor, "actor" => follow_activity.actor,
"object" => follow_activity.data["object"], "object" => follow_activity.data["object"],
@ -485,14 +525,7 @@ def prepare_outgoing(%{"type" => "Accept"} = data) do
end end
def prepare_outgoing(%{"type" => "Reject"} = data) do def prepare_outgoing(%{"type" => "Reject"} = data) do
follow_activity_id = with follow_activity <- Activity.normalize(data["object"]) do
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
object = %{ object = %{
"actor" => follow_activity.actor, "actor" => follow_activity.actor,
"object" => follow_activity.data["object"], "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. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) 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 with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end

View file

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

View file

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

View file

@ -35,7 +35,8 @@ defmodule Pleroma.Web.Endpoint do
parsers: [:urlencoded, :multipart, :json], parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"], pass: ["*/*"],
json_decoder: Jason, 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) plug(Plug.MethodOverride)

View file

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

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification, Stats} alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -11,6 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
import Ecto.Query import Ecto.Query
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
action_fallback(:errors) action_fallback(:errors)
def create_app(conn, params) do def create_app(conn, params) do
@ -125,7 +127,7 @@ def masto_instance(conn, _params) do
response = %{ response = %{
uri: Web.base_url(), uri: Web.base_url(),
title: Keyword.get(@instance, :name), 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)})", version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email), email: Keyword.get(@instance, :email),
urls: %{ urls: %{
@ -428,16 +430,43 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
render(conn, AccountView, "relationships.json", %{user: user, targets: targets}) render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do def update_media(%{assigns: %{user: _}} = conn, data) do
with {:ok, object} <- ActivityPub.upload(file) 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 = data =
object.data new_data
|> Map.put("id", object.id) |> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data}) render(conn, StatusView, "attachment.json", %{attachment: data})
end end
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 def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^likes) q = from(u in User, where: u.ap_id in ^likes)
@ -873,7 +902,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
}, },
compose: %{ compose: %{
me: "#{user.id}", me: "#{user.id}",
default_privacy: "public", default_privacy: user.info["default_scope"] || "public",
default_sensitive: false default_sensitive: false
}, },
media_attachments: %{ media_attachments: %{
@ -1070,4 +1099,38 @@ def errors(conn, _) do
|> put_status(500) |> put_status(500)
|> json("Something went wrong") |> json("Something went wrong")
end 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 end

View file

@ -14,6 +14,18 @@ def render("account.json", %{user: user}) do
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user) 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), id: to_string(user.id),
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
@ -30,7 +42,7 @@ def render("account.json", %{user: user}) do
avatar_static: image, avatar_static: image,
header: header, header: header,
header_static: header, header_static: header,
emojis: [], emojis: emojis,
fields: [], fields: [],
source: %{ source: %{
note: "", note: "",

View file

@ -54,8 +54,7 @@ def render(
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object, uri: object,
# TODO: This might be wrong, check with mastodon. url: object,
url: nil,
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_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_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
content: HtmlSanitizeEx.basic_html(object["content"]), content: render_content(object),
created_at: created_at, created_at: created_at,
reblogs_count: announcement_count, reblogs_count: announcement_count,
favourites_count: like_count, favourites_count: like_count,
@ -170,7 +169,8 @@ def render("attachment.json", %{attachment: attachment}) do
remote_url: href, remote_url: href,
preview_url: MediaProxy.url(href), preview_url: MediaProxy.url(href),
text_url: href, text_url: href,
type: type type: type,
description: attachment["name"]
} }
end end
@ -207,4 +207,21 @@ def get_visibility(object) do
"direct" "direct"
end end
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 end

View file

@ -4,8 +4,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.Web alias Pleroma.Web
@instance Application.get_env(:pleroma, :instance)
def schemas(conn, _params) do def schemas(conn, _params) do
response = %{ response = %{
links: [ links: [
@ -21,20 +19,23 @@ def schemas(conn, _params) do
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
def nodeinfo(conn, %{"version" => "2.0"}) do 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() stats = Stats.get_stats()
response = %{ response = %{
version: "2.0", version: "2.0",
software: %{ software: %{
name: "pleroma", name: "pleroma",
version: Keyword.get(@instance, :version) version: Keyword.get(instance, :version)
}, },
protocols: ["ostatus", "activitypub"], protocols: ["ostatus", "activitypub"],
services: %{ services: %{
inbound: [], inbound: [],
outbound: [] outbound: []
}, },
openRegistrations: Keyword.get(@instance, :registrations_open), openRegistrations: Keyword.get(instance, :registrations_open),
usage: %{ usage: %{
users: %{ users: %{
total: stats.user_count || 0 total: stats.user_count || 0
@ -42,7 +43,16 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
localPosts: stats.status_count || 0 localPosts: stats.status_count || 0
}, },
metadata: %{ 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: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.recipients || []) |> get_mentions 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']}, {:"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 def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), 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 {:ok, delete} <- ActivityPub.delete(object, false) do
delete delete
end end

View file

@ -89,7 +89,7 @@ def handle_incoming(xml_string) do
def make_share(entry, doc, retweeted_activity) do def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc), 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), id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -107,7 +107,7 @@ def handle_share(entry, doc) do
def make_favorite(entry, doc, favorited_activity) do def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc), 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), id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity} {:ok, activity}

View file

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

View file

@ -127,6 +127,7 @@ def user_fetcher(username) do
get("/notifications/:id", MastodonAPIController, :get_notification) get("/notifications/:id", MastodonAPIController, :get_notification)
post("/media", MastodonAPIController, :upload) post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
get("/lists", MastodonAPIController, :get_lists) get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list) get("/lists/:id", MastodonAPIController, :get_list)
@ -140,6 +141,8 @@ def user_fetcher(username) do
get("/domain_blocks", MastodonAPIController, :domain_blocks) get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain) post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain) delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/suggestions", MastodonAPIController, :suggestions)
end end
scope "/api/web", Pleroma.Web.MastodonAPI do 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("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation) get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
if @registrations_open do post("/account/register", TwitterAPI.Controller, :register)
post("/account/register", TwitterAPI.Controller, :register)
end
get("/search", TwitterAPI.Controller, :search) get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline) get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
@ -355,6 +356,7 @@ def user_fetcher(username) do
end end
scope "/", Fallback do scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector) get("/*path", RedirectController, :redirector)
end end
end end
@ -369,4 +371,8 @@ def redirector(conn, _params) do
|> send_file(200, "priv/static/index.html") |> send_file(200, "priv/static/index.html")
end end
end end
def registration_page(conn, params) do
redirector(conn, params)
end
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) user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || [] 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 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)}) send(socket.transport_pid, {:text, represent_update(item, user)})

View file

@ -99,6 +99,10 @@ def do_remote_follow(conn, %{
conn conn
|> render("followed.html", %{error: false}) |> render("followed.html", %{error: false})
else else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
_e -> _e ->
conn conn
|> render("follow_login.html", %{ |> render("follow_login.html", %{
@ -117,6 +121,11 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
conn conn
|> render("followed.html", %{error: false}) |> render("followed.html", %{error: false})
else else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
e -> e ->
Logger.debug("Remote follow failed with error #{inspect(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 end
@instance Application.get_env(:pleroma, :instance) @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 def config(conn, _params) do
case get_format(conn) do case get_format(conn) do
"xml" -> "xml" ->
@ -148,9 +159,24 @@ def config(conn, _params) do
json(conn, %{ json(conn, %{
site: %{ site: %{
name: Keyword.get(@instance, :name), name: Keyword.get(@instance, :name),
description: Keyword.get(@instance, :description),
server: Web.base_url(), server: Web.base_url(),
textlimit: to_string(Keyword.get(@instance, :limit)), textlimit: to_string(Keyword.get(@instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1") closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"),
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 end

View file

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

View file

@ -1,11 +1,13 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do 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.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Web.{OStatus, CommonAPI}
import Ecto.Query import Ecto.Query
@instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@registrations_open Keyword.get(@instance, :registrations_open)
def create_status(%User{} = user, %{"status" => _} = data) do def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data) CommonAPI.post(user, data)
@ -120,6 +122,8 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
end end
def register_user(params) do def register_user(params) do
tokenString = params["token"]
params = %{ params = %{
nickname: params["nickname"], nickname: params["nickname"],
name: params["fullname"], name: params["fullname"],
@ -129,17 +133,33 @@ def register_user(params) do
password_confirmation: params["confirm"] 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 cond do
{:ok, user} @registrations_open || (!is_nil(token) && !token.used) ->
else changeset = User.register_changeset(%User{}, params)
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{: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
end end

View file

@ -431,6 +431,19 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
user user
end 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), with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)

View file

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

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.TwitterAPI.UserView do defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.User alias Pleroma.User
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy 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) 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 = %{ data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(), "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, "favourites_count" => 0,
"followers_count" => user_info[:follower_count], "followers_count" => user_info[:follower_count],
"following" => following, "following" => following,
@ -39,6 +50,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"friends_count" => user_info[:following_count], "friends_count" => user_info[:following_count],
"id" => user.id, "id" => user.id,
"name" => user.name, "name" => user.name,
"name_html" => HtmlSanitizeEx.strip_tags(user.name) |> Formatter.emojify(emoji),
"profile_image_url" => image, "profile_image_url" => image,
"profile_image_url_https" => image, "profile_image_url_https" => image,
"profile_image_url_profile_size" => 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(), "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(), "background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
"is_local" => user.local, "is_local" => user.local,
"locked" => !!user.info["locked"] "locked" => !!user.info["locked"],
"default_scope" => user.info["default_scope"] || "public"
} }
if assigns[:token] do if assigns[:token] do

View file

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

View file

@ -25,6 +25,7 @@
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [: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"}, "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"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "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"}, "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 CACHE MANIFEST
#ver:2018-4-9 21:57:37 #ver:2018-8-12 18:01:32
#plugin:4.8.4 #plugin:4.8.4
CACHE: CACHE:
@ -13,33 +13,46 @@ CACHE:
/packs/features/home_timeline.js /packs/features/home_timeline.js
/packs/features/public_timeline.js /packs/features/public_timeline.js
/packs/features/community_timeline.js /packs/features/community_timeline.js
/packs/features/favourited_statuses.js /packs/features/direct_timeline.js
/packs/features/list_timeline.js /packs/features/pinned_statuses.js
/packs/features/domain_blocks.js
/packs/features/following.js /packs/features/following.js
/packs/features/followers.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/hashtag_timeline.js
/packs/features/status.js /packs/features/status.js
/packs/features/account_gallery.js /packs/features/lists.js
/packs/features/blocks.js /packs/modals/report_modal.js
/packs/features/getting_started.js
/packs/features/follow_requests.js /packs/features/follow_requests.js
/packs/features/mutes.js
/packs/features/blocks.js
/packs/features/reblogs.js /packs/features/reblogs.js
/packs/features/favourites.js /packs/features/favourites.js
/packs/features/getting_started.js
/packs/features/keyboard_shortcuts.js /packs/features/keyboard_shortcuts.js
/packs/modals/mute_modal.js
/packs/features/generic_not_found.js /packs/features/generic_not_found.js
/packs/features/list_editor.js /packs/features/list_editor.js
/packs/modals/embed_modal.js
/packs/status/media_gallery.js /packs/status/media_gallery.js
/packs/containers/media_container.js
/packs/share.js /packs/share.js
/packs/application.js /packs/application.js
/packs/about.js /packs/about.js
/packs/public.js
/packs/mailer.js /packs/mailer.js
/packs/mastodon-light.js
/packs/contrast.js
/packs/default.js /packs/default.js
/packs/public.js
/packs/admin.js /packs/admin.js
/packs/common.js /packs/common.js
/packs/common.css /packs/common.css
/packs/mailer.css /packs/mailer.css
/packs/default.css /packs/default.css
/packs/contrast.css
/packs/mastodon-light.css
/packs/manifest.json /packs/manifest.json
NETWORK: 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.

Binary file not shown.

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

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