- [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
- LDAP authentication
- External OAuth provider authentication
+- Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html)
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction
- Federation: Expand the audience of delete activities to all recipients of the deleted object
- Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default
+- Configuration: Default log level in `prod` environment is now set to `warn`
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Admin API: Move the user related API to `api/pleroma/admin/users`
http: [port: 4000],
protocol: "http"
+config :phoenix, serve_endpoints: true
+
# Do not print debug messages in production
-config :logger, level: :info
+config :logger, level: :warn
# ## SSL Support
#
--- /dev/null
+import Config
end
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
- Keyword.get(options, opt) ||
- case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do
- "\n" ->
- case defval do
- nil -> get_option(options, opt, prompt, defval)
- defval -> defval
- end
-
- opt ->
- opt |> String.trim()
- end
+ Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
end
+ def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
+ prompt_message = "#{prompt} [#{defname || defval}]"
+
+ input =
+ if mix_shell?(),
+ do: Mix.shell().prompt(prompt_message),
+ else: :io.get_line(prompt_message)
+
+ case input do
+ "\n" ->
+ case defval do
+ nil ->
+ shell_prompt(prompt, defval, defname)
+
+ defval ->
+ defval
+ end
+
+ input ->
+ String.trim(input)
+ end
+ end
+
+ def shell_yes?(message) do
+ if mix_shell?(),
+ do: Mix.shell().yes?("Continue?"),
+ else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
+ end
+
+ def shell_info(message) do
+ if mix_shell?(),
+ do: Mix.shell().info(message),
+ else: IO.puts(message)
+ end
+
+ def shell_error(message) do
+ if mix_shell?(),
+ do: Mix.shell().error(message),
+ else: IO.puts(:stderr, message)
+ end
+
+ @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)"
+ def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
+
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
dbpass: dbpass
)
- Mix.shell().info(
+ Common.shell_info(
"Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs."
)
File.write(config_path, result_config)
- Mix.shell().info("Writing #{psql_path}.")
+ Common.shell_info("Writing #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(indexable)
- Mix.shell().info(
+ Common.shell_info(
"\n" <>
"""
To get started:
end
)
else
- Mix.shell().error(
+ Common.shell_error(
"The task would have overwritten the following files:\n" <>
(Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
if File.exists?(robots_txt_path) do
File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
- Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak")
+ Common.shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")
end
File.write(robots_txt_path, robots_txt)
- Mix.shell().info("Writing #{robots_txt_path}.")
+ Common.shell_info("Writing #{robots_txt_path}.")
end
end
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
- {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
+ {:error, e} -> Common.shell_error("Error while following #{target}: #{inspect(e)}")
end
end
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
- {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
+ {:error, e} -> Common.shell_error("Error while following #{target}: #{inspect(e)}")
end
end
end
Pleroma.Config.put([Upload, :uploader], uploader)
end
- Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}")
+ Common.shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}")
if delete? do
- Mix.shell().info(
+ Common.shell_info(
"Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
)
|> Enum.filter(& &1)
total_count = length(uploads)
- Mix.shell().info("Found #{total_count} uploads")
+ Common.shell_info("Found #{total_count} uploads")
uploads
|> Task.async_stream(
:ok
error ->
- Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
+ Common.shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
end
end,
timeout: 150_000
# credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation
|> Enum.reduce(0, fn done, count ->
count = count + length(done)
- Mix.shell().info("Uploaded #{count}/#{total_count} files")
+ Common.shell_info("Uploaded #{count}/#{total_count} files")
count
end)
- Mix.shell().info("Done!")
+ Common.shell_info("Done!")
end
end
admin? = Keyword.get(options, :admin, false)
assume_yes? = Keyword.get(options, :assume_yes, false)
- Mix.shell().info("""
+ Common.shell_info("""
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
- admin: #{if(admin?, do: "true", else: "false")}
""")
- proceed? = assume_yes? or Mix.shell().yes?("Continue?")
+ proceed? = assume_yes? or Common.shell_yes?("Continue?")
if proceed? do
Common.start_pleroma()
changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset)
- Mix.shell().info("User #{nickname} created")
+ Common.shell_info("User #{nickname} created")
if moderator? do
run(["set", nickname, "--moderator"])
run(["reset_password", nickname])
end
else
- Mix.shell().info("User will not be created.")
+ Common.shell_info("User will not be created.")
end
end
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.perform(:delete, user)
- Mix.shell().info("User #{nickname} deleted.")
+ Common.shell_info("User #{nickname} deleted.")
else
_ ->
- Mix.shell().error("No local user #{nickname}")
+ Common.shell_error("No local user #{nickname}")
end
end
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated)
- Mix.shell().info(
+ Common.shell_info(
"Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"
)
else
_ ->
- Mix.shell().error("No user #{nickname}")
+ Common.shell_error("No user #{nickname}")
end
end
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
- Mix.shell().info("Generated password reset token for #{user.nickname}")
+ Common.shell_info("Generated password reset token for #{user.nickname}")
IO.puts(
"URL: #{
)
else
_ ->
- Mix.shell().error("No local user #{nickname}")
+ Common.shell_error("No local user #{nickname}")
end
end
Common.start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
- Mix.shell().info("Deactivating #{user.nickname}")
+ Common.shell_info("Deactivating #{user.nickname}")
User.deactivate(user)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn friend ->
user = User.get_cached_by_id(user.id)
- Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
+ Common.shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
- Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
+ Common.shell_info("Successfully unsubscribed all followers from #{user.nickname}")
end
else
_ ->
- Mix.shell().error("No user #{nickname}")
+ Common.shell_error("No user #{nickname}")
end
end
end
else
_ ->
- Mix.shell().error("No local user #{nickname}")
+ Common.shell_error("No local user #{nickname}")
end
end
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
- Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
+ Common.shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
- Mix.shell().error("Could not change user tags for #{nickname}")
+ Common.shell_error("Could not change user tags for #{nickname}")
end
end
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
- Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
+ Common.shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
- Mix.shell().error("Could not change user tags for #{nickname}")
+ Common.shell_error("Could not change user tags for #{nickname}")
end
end
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
- Mix.shell().info(
+ Common.shell_info(
"Generated user invite token " <> String.replace(invite.invite_type, "_", " ")
)
IO.puts(url)
else
error ->
- Mix.shell().error("Could not create invite token: #{inspect(error)}")
+ Common.shell_error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
Common.start_pleroma()
- Mix.shell().info("Invites list:")
+ Common.shell_info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
- Mix.shell().info(
+ Common.shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
- Mix.shell().info("Invite for token #{token} was revoked.")
+ Common.shell_info("Invite for token #{token} was revoked.")
else
- _ -> Mix.shell().error("No invite found with token #{token}")
+ _ -> Common.shell_error("No invite found with token #{token}")
end
end
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, _} = User.delete_user_activities(user)
- Mix.shell().info("User #{nickname} statuses deleted.")
+ Common.shell_info("User #{nickname} statuses deleted.")
else
_ ->
- Mix.shell().error("No local user #{nickname}")
+ Common.shell_error("No local user #{nickname}")
end
end
message = if user.info.confirmation_pending, do: "needs", else: "doesn't need"
- Mix.shell().info("#{nickname} #{message} confirmation.")
+ Common.shell_info("#{nickname} #{message} confirmation.")
else
_ ->
- Mix.shell().error("No local user #{nickname}")
+ Common.shell_error("No local user #{nickname}")
end
end
{:ok, user} = User.update_and_set_cache(user_cng)
- Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
+ Common.shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
user
end
{:ok, user} = User.update_and_set_cache(user_cng)
- Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}")
+ Common.shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
user
end
{:ok, user} = User.update_and_set_cache(user_cng)
- Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}")
+ Common.shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
user
end
end
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
- order_by: [desc: p.updated_at]
+ order_by: [desc: p.updated_at],
+ preload: [conversation: [:users]]
)
|> Pleroma.Pagination.fetch_paginated(params)
- |> Repo.preload(conversation: [:users])
end
def for_user_with_last_activity_id(user, params \\ %{}) do
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
{:ok, results} ->
- grouped = Enum.group_by(results, &File.dir?/1)
+ grouped =
+ Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
+
packs = grouped[true] || []
files = grouped[false] || []
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ReleaseTasks do
+ @repo Pleroma.Repo
+
+ def run(args) do
+ Mix.Tasks.Pleroma.Common.start_pleroma()
+ [task | args] = String.split(args)
+
+ case task do
+ "migrate" -> migrate()
+ "create" -> create()
+ "rollback" -> rollback(String.to_integer(Enum.at(args, 0)))
+ task -> mix_task(task, args)
+ end
+ end
+
+ defp mix_task(task, args) do
+ {:ok, modules} = :application.get_key(:pleroma, :modules)
+
+ module =
+ Enum.find(modules, fn module ->
+ module = Module.split(module)
+
+ match?(["Mix", "Tasks", "Pleroma" | _], module) and
+ String.downcase(List.last(module)) == task
+ end)
+
+ if module do
+ module.run(args)
+ else
+ IO.puts("The task #{task} does not exist")
+ end
+ end
+
+ def migrate do
+ {:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, :up, all: true))
+ end
+
+ def rollback(version) do
+ {:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, :down, to: version))
+ end
+
+ def create do
+ case @repo.__adapter__.storage_up(@repo.config) do
+ :ok ->
+ IO.puts("The database for #{inspect(@repo)} has been created")
+
+ {:error, :already_up} ->
+ IO.puts("The database for #{inspect(@repo)} has already been created")
+
+ {:error, term} when is_binary(term) ->
+ IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
+
+ {:error, term} ->
+ IO.puts(
+ :stderr,
+ "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
+ )
+ end
+ end
+end
def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
reply = Object.normalize(reply_id)
- if reply.data["type"] == "Question" and object["name"] do
+ if reply && (reply.data["type"] == "Question" and object["name"]) do
Map.put(object, "type", "Answer")
else
object
],
main: "readme",
output: "priv/static/doc"
+ ],
+ releases: [
+ pleroma: [
+ include_executables_for: [:unix],
+ applications: [ex_syslogger: :load, syslog: :load],
+ steps: [:assemble, ©_pleroma_ctl/1]
+ ]
]
]
end
+ def copy_pleroma_ctl(%{path: target_path} = release) do
+ File.cp!("./rel/pleroma_ctl", Path.join([target_path, "bin", "pleroma_ctl"]))
+ release
+ end
+
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
defmodule Pleroma.Repo.Migrations.AddFollowerAddressToUser do
use Ecto.Migration
- import Ecto.Query
- import Supervisor.Spec
- alias Pleroma.{Repo, User}
def up do
alter table(:users) do
add :follower_address, :string, unique: true
end
-
- # Not needed anymore for new setups.
- # flush()
-
- # children = [
- # # Start the endpoint when the application starts
- # supervisor(Pleroma.Web.Endpoint, [])
- # ]
- # opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
- # Supervisor.start_link(children, opts)
-
- # Enum.each(Repo.all(User), fn (user) ->
- # if !user.follower_address do
- # cs = Ecto.Changeset.change(user, %{follower_address: User.ap_followers(user)})
- # Repo.update!(cs)
- # end
- # end)
end
def down do
--- /dev/null
+#!/bin/sh
+
+# Sets and enables heart (recommended only in daemon mode)
+# if [ "$RELEASE_COMMAND" = "daemon" ] || [ "$RELEASE_COMMAND" = "daemon_iex" ]; then
+# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
+# export HEART_COMMAND
+# export ELIXIR_ERL_OPTIONS="-heart"
+# fi
+
+# Set the release to work across nodes
+export RELEASE_DISTRIBUTION=name
+export RELEASE_NODE=<%= @release.name %>@127.0.0.1
--- /dev/null
+#!/bin/sh
+# XXX: This should be removed when elixir's releases get custom command support
+if [ -z "$1" ] || [ "$1" == "help" ]; then
+ echo "Usage: $(basename "$0") COMMAND [ARGS]
+
+ The known commands are:
+
+ create Create database schema (needs to be executed only once)
+ migrate Execute database migrations (needs to be done after updates)
+ rollback [VERSION] Rollback database migrations (needs to be done before downgrading)
+
+ and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is
+ equivalent to \`$(basename "$0") user COMMAND\`
+"
+else
+ SCRIPT=$(readlink -f "$0")
+ SCRIPTPATH=$(dirname "$SCRIPT")
+ $SCRIPTPATH/pleroma eval 'Pleroma.ReleaseTasks.run("'"$*"'")'
+fi
--- /dev/null
+## Customize flags given to the VM: http://erlang.org/doc/man/erl.html
+## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
+
+## Number of dirty schedulers doing IO work (file, sockets, etc)
+##+SDio 5
+
+## Increase number of concurrent ports/sockets
+##+Q 65536
+
+## Tweak GC to run more often
+##-env ERL_FULLSWEEP_AFTER 10
object2 = Pleroma.Object.normalize(activity_two)
object3 = Pleroma.Object.normalize(activity_three)
+ user = Repo.get(Pleroma.User, user.id)
+
assert participation_one.conversation.ap_id == object3.data["context"]
assert participation_two.conversation.ap_id == object2.data["context"]
+ assert participation_one.conversation.users == [user]
# Pagination
assert [participation_one] = Participation.for_user(user, %{"limit" => 1})
encoded = url(url)
assert decode_result(encoded) == url
end
+
+ test "does not change whitelisted urls" do
+ upload_config = Pleroma.Config.get([Pleroma.Upload])
+ media_url = "https://media.pleroma.social"
+ Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
+ Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"])
+ Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
+
+ url = "#{media_url}/static/logo.png"
+ encoded = url(url)
+
+ assert String.starts_with?(encoded, media_url)
+
+ Pleroma.Config.put([Pleroma.Upload], upload_config)
+ end
end
describe "when disabled" do
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
end
+ test "it does not crash if the object in inReplyTo can't be fetched" do
+ data =
+ File.read!("test/fixtures/mastodon-post-activity.json")
+ |> Poison.decode!()
+
+ object =
+ data["object"]
+ |> Map.put("inReplyTo", "https://404.site/whatever")
+
+ data =
+ data
+ |> Map.put("object", object)
+
+ {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
+ end
+
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
end
end
+ describe "media upload" do
+ setup do
+ upload_config = Pleroma.Config.get([Pleroma.Upload])
+ proxy_config = Pleroma.Config.get([:media_proxy])
+
+ on_exit(fn ->
+ Pleroma.Config.put([Pleroma.Upload], upload_config)
+ Pleroma.Config.put([:media_proxy], proxy_config)
+ end)
+
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+
+ image = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ [conn: conn, image: image]
+ end
+
+ test "returns uploaded image", %{conn: conn, image: image} do
+ desc = "Description of the image"
+
+ media =
+ conn
+ |> post("/api/v1/media", %{"file" => image, "description" => desc})
+ |> json_response(:ok)
+
+ assert media["type"] == "image"
+ assert media["description"] == desc
+ assert media["id"]
+
+ object = Repo.get(Object, media["id"])
+ assert object.data["actor"] == User.ap_id(conn.assigns[:user])
+ end
+
+ test "returns proxied url when media proxy is enabled", %{conn: conn, image: image} do
+ Pleroma.Config.put([Pleroma.Upload, :base_url], "https://media.pleroma.social")
+
+ proxy_url = "https://cache.pleroma.social"
+ Pleroma.Config.put([:media_proxy, :enabled], true)
+ Pleroma.Config.put([:media_proxy, :base_url], proxy_url)
+
+ media =
+ conn
+ |> post("/api/v1/media", %{"file" => image})
+ |> json_response(:ok)
+
+ assert String.starts_with?(media["url"], proxy_url)
+ end
+
+ test "returns media url when proxy is enabled but media url is whitelisted", %{
+ conn: conn,
+ image: image
+ } do
+ media_url = "https://media.pleroma.social"
+ Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
+
+ Pleroma.Config.put([:media_proxy, :enabled], true)
+ Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
+ Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"])
+
+ media =
+ conn
+ |> post("/api/v1/media", %{"file" => image})
+ |> json_response(:ok)
+
+ assert String.starts_with?(media["url"], media_url)
+ end
+ end
+
describe "locked accounts" do
test "/api/v1/follow_requests works" do
user = insert(:user, %{info: %User.Info{locked: true}})
assert id == user.id
end
- test "media upload", %{conn: conn} do
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- desc = "Description of the image"
-
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> post("/api/v1/media", %{"file" => file, "description" => desc})
-
- assert media = json_response(conn, 200)
-
- assert media["type"] == "image"
- assert media["description"] == desc
- assert media["id"]
-
- object = Repo.get(Object, media["id"])
- assert object.data["actor"] == User.ap_id(user)
- end
-
test "mascot upload", %{conn: conn} do
user = insert(:user)