Commit eb82cc63 authored by hakabahitoyo's avatar hakabahitoyo
Browse files

Merge remote-tracking branch 'official/develop' into deploy/20181230

parents e241c2e1 1344e34e
Pipeline #9097 failed with stages
in 4 minutes and 8 seconds
......@@ -34,7 +34,8 @@
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [],
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: true,
proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
......@@ -369,6 +370,17 @@
rel: false
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost",
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
# 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"
......@@ -17,6 +17,8 @@
# Print only warnings and errors during test
config :logger, level: :warn
config :pleroma, Pleroma.Upload, filters: [], link_name: false
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Test
# Custom emoji
To add custom emoji:
* Add the image file(s) to `priv/static/emoji/custom`
* In case of conflicts: add the desired shortcode with the path to `config/custom_emoji.txt`, comma-separated and one per line
* Force recompilation (``mix clean && mix compile``)
image files (in `/priv/static/emoji/custom`): `happy.png` and `sad.png`
content of `config/custom_emoji.txt`:
happy, /emoji/custom/happy.png
sad, /emoji/custom/sad.png
The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon.
......@@ -20,6 +20,12 @@ Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance.
## Attachments
Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment.
## Accounts
- `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc.
# Message Rewrite Facility configuration
The Message Rewrite Facility (MRF) is a subsystem that is implemented as a series of hooks that allows the administrator to rewrite or discard messages.
Possible uses include:
* marking incoming messages with media from a given account or instance as sensitive
* rejecting messages from a specific instance
* removing/unlisting messages from the public timelines
* removing media from messages
* sending only public messages to a specific instance
The MRF provides user-configurable policies. The default policy is `NoOpPolicy`, which disables the MRF functionality. Pleroma also includes an easy to use policy called `SimplePolicy` which maps messages matching certain pre-defined criterion to actions built into the policy module.
It is possible to use multiple, active MRF policies at the same time.
## Quarantine Instances
You have the ability to prevent from private / followers-only messages from federating with specific instances. Which means they will only get the public or unlisted messages from your instance.
If, for example, you're using `MIX_ENV=prod` aka using production mode, you would open your configuration file located in `config/prod.secret.exs` and edit or add the option under your `:instance` config object. Then you would specify the instance within quotes.
config :pleroma, :instance,
quarantined_instances: ["instance.example", "other.example"]
## Using `SimplePolicy`
`SimplePolicy` is capable of handling most common admin tasks.
To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this:
config :pleroma, :instance,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `reject`: Servers in this group will have their messages rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
Servers should be configured as lists.
### Example
This example will enable `SimplePolicy`, block media from ``, mark media as NSFW from `` and ``, reject messages from `` and remove messages from `` from the federated timeline:
config :pleroma, :instance,
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
config :pleroma, :mrf_simple,
media_removal: [""],
media_nsfw: ["", ""],
reject: [""],
federated_timeline_removal: [""]
### Use with Care
The effects of MRF policies can be very drastic. It is important to use this functionality carefully. Always try to talk to an admin before writing an MRF policy concerning their instance.
## Writing your own MRF Policy
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting.
For example, here is a sample policy module which rewrites all messages to "new message content":
# This is a sample MRF policy which rewrites all Notes to have "new message
# content."
defmodule Site.RewritePolicy do
@behavior Pleroma.Web.ActivityPub.MRF
# Catch messages which contain Note objects with actual data to filter.
# Capture the object as `object`, the message content as `content` and the
# message itself as `message`.
@impl true
def filter(%{"type" => Create", "object" => {"type" => "Note", "content" => content} = object} = message)
when is_binary(content) do
# Subject / CW is stored as summary instead of `name` like other AS2 objects
# because of Mastodon doing it that way.
summary = object["summary"]
# Message edits go here.
content = "new message content"
# Assemble the mutated object.
object =
|> Map.put("content", content)
|> Map.put("summary", summary)
# Assemble the mutated message.
message = Map.put(message, "object", object)
{:ok, message}
# Let all other messages through without modifying them.
@impl true
def filter(message), do: {:ok, message}
If you save this file as `lib/site/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so:
config :pleroma, :instance,
rewrite_policy: [
Please note that the Pleroma developers consider custom MRF policy modules to fall under the purview of the AGPL. As such, you are obligated to release the sources to your custom MRF policy modules upon request.
\ No newline at end of file
......@@ -108,3 +108,11 @@ See [Admin-API](
* Response: JSON string. Returns the user flavour or the default one.
* Example response: "glitch"
* Note: This is intended to be used only by mastofe
## `/api/pleroma/notifications/read`
### Mark a single notification as read
* Method `POST`
* Authentication: required
* Params:
* `id`: notifications's id
* Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}`
......@@ -6,6 +6,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
......@@ -330,3 +331,26 @@ config :auto_linker,
rel: false
## :ldap
Use LDAP for user authentication. When a user logs in to the Pleroma
instance, the name and password will be verified by trying to authenticate
(bind) to an LDAP server. If a user exists in the LDAP directory but there
is no account with the same name yet on the Pleroma instance then a new
Pleroma account will be created with the same name as the LDAP user name.
* `enabled`: enables LDAP authentication
* `host`: LDAP server hostname
* `port`: LDAP port, e.g. 389 or 636
* `ssl`: true to use SSL, usually implies the port 636
* `sslopts`: additional SSL options
* `tls`: true to start TLS, usually implies the port 389
* `tlsopts`: additional TLS options
* `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
## Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication
# Static Directory
Static frontend files are shipped in `priv/static/` and tracked by version control in this repository. If you want to overwrite or update these without the possibility of merge conflicts, you can write your custom versions to `instance/static/`.
config :pleroma, :instance,
static_dir: "instance/static/",
You can overwrite this value in your configuration to use a different static instance directory.
## robots.txt
By default, the `robots.txt` that ships in `priv/static/` is permissive. It allows well-behaved search engines to index all of your instance's URIs.
If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance static directory.
mix pleroma.robots_txt disallow_all
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxt do
use Mix.Task
@shortdoc "Generate robots.txt"
@moduledoc """
Generates robots.txt
## Overwrite robots.txt to disallow all
mix pleroma.robots_txt disallow_all
This will write a robots.txt that will hide all paths on your instance
from search engines and other robots that obey robots.txt
def run(["disallow_all"]) do
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
if !File.exists?(static_dir) do
robots_txt_path = Path.join(static_dir, "robots.txt")
robots_txt_content = "User-Agent: *\nDisallow: /\n"
File.write!(robots_txt_path, robots_txt_content, [:write])
......@@ -95,6 +95,13 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])
......@@ -137,6 +144,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", [])
......@@ -13,6 +13,7 @@ defmodule Pleroma.Notification do
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query
import Ecto.Changeset
schema "notifications" do
field(:seen, :boolean, default: false)
......@@ -22,6 +23,11 @@ defmodule Pleroma.Notification do
def changeset(%Notification{} = notification, attrs) do
|> cast(attrs, [:seen])
# TODO: Make generic and unify (see activity_pub.ex)
defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: < ^max_id)
......@@ -68,6 +74,14 @@ def set_read_up_to(%{id: user_id} = _user, id) do
Repo.update_all(query, [])
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
|> changeset(%{seen: true})
|> Repo.update()
def get(%{id: user_id} = _user, id) do
query =
......@@ -21,7 +21,8 @@ def file_path(path) do
@only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js)
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
def init(opts) do
......@@ -24,6 +24,18 @@ def init(_opts) do
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn =
case fetch_query_params(conn) do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
conn ->
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
......@@ -311,7 +311,25 @@ defp build_resp_content_disposition_header(headers, opts) do
if attachment? do
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
name =
try do
{{"content-disposition", content_disposition_string}, _} =
List.keytake(headers, "content-disposition", 0)
[name | _] =
content_disposition_string || "",
capture: :all_but_first
MatchError -> Keyword.get(opts, :attachment_name, "attachment")
disposition = "attachment; filename=\"#{name}\""
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
......@@ -70,7 +70,7 @@ def store(upload, opts \\ []) do
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(opts.base_url, url_spec)
"href" => url_from_spec(upload, opts.base_url, url_spec)
"name" => Map.get(opts, :description) ||
......@@ -219,14 +219,18 @@ defp tempfile_for_image(data) do
defp url_from_spec(base_url, {:file, path}) do
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
|> URI.encode(&char_unescaped?/1)
URI.encode(path, &char_unescaped?/1) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
[base_url, "media", path]
|> Path.join()
defp url_from_spec(_base_url, {:url, url}), do: url
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
......@@ -1394,4 +1394,8 @@ defp paginate(query, page, page_size) do
offset: ^((page - 1) * page_size)
def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in
......@@ -21,6 +21,7 @@ defmodule Pleroma.User.Info do
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
......@@ -259,4 +260,16 @@ def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do
moderator: is_moderator
def add_reblog_mute(info, ap_id) do
params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
cast(info, params, [:muted_reblogs])
def remove_reblog_mute(info, ap_id) do
params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
cast(info, params, [:muted_reblogs])
......@@ -310,7 +310,7 @@ def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
to =["to"] || [] ++["cc"] || []
to = (["to"] || []) ++ (["cc"] || [])
with {:ok, object, activity} <- Object.delete(object),
data <- %{
......@@ -370,20 +370,38 @@ def flag(
content: content
} = params
) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
|> make_flag_data(additional)
|> insert(local)
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
Map.merge(additional, %{"to" => [], "cc" => []})
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
:ok <- maybe_federate(activity) do
Enum.each(User.all_superusers(), fn superuser ->
|>, account, statuses, content)
|> Pleroma.Mailer.deliver_async()
{:ok, activity}
def fetch_activities_for_context(context, opts \\ %{}) do
......@@ -679,6 +697,18 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids})
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
muted_reblogs = info.muted_reblogs || []
activity in query,
where: fragment("not ?->>'type' = 'Announce'",,
where: fragment("not ? = ANY(?)",, ^muted_reblogs)
defp restrict_muted_reblogs(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
......@@ -706,6 +736,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
def fetch_activities(recipients, opts \\ %{}) do
......@@ -344,6 +344,40 @@ defp get_follow_activity(follow_object, followed) do
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <-
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
_ -> {:cont, nil}
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{
"cc" => [account.ap_id]
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
......@@ -621,7 +621,13 @@ def make_create_data(params, additional) do
#### Flag-related helpers
def make_flag_data(params, additional) do
status_ap_ids = || [], & &["id"])