The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## 2023.04
+
+## Added
+- Nodeinfo keys for unauthenticated timeline visibility
+- Option to disable federated timeline
+- Option to make the bubble timeline publicly accessible
+- Ability to swap between installed standard frontends
+ - *mastodon frontends are still not counted as standard frontends due to the complexity in serving them correctly*.
+
+### Upgrade Notes
+- Elixir 1.14 is now required. If your distribution does not package this, you can
+ use [asdf](https://asdf-vm.com/). At time of writing, elixir 1.14.3 / erlang 25.3
+ is confirmed to work.
+
## 2023.03
## Fixed
### Removed
- Possibility of using the `style` parameter on `span` elements. This will break certain MFM parameters.
+- Option for "default" image description.
## 2023.02
-FROM hexpm/elixir:1.13.4-erlang-24.3.4.5-alpine-3.15.6
+FROM hexpm/elixir:1.14.3-erlang-25.3-alpine-3.17.2
ENV MIX_ENV=prod
ENV ERL_EPMD_ADDRESS=127.0.0.1
### Docker
Docker installation is supported via [this setup](https://docs.akkoma.dev/stable/installation/docker_en/)
+### Packages
+Akkoma is packaged for [YunoHost](https://yunohost.org) and can be found and installed from the [YunoHost app catalogue](https://yunohost.org/#/apps).
+
### Compilation Troubleshooting
If you ever encounter compilation issues during the updating of Akkoma, you can try these commands and see if they fix things:
link_name: false,
proxy_remote: false,
filename_display_max_length: 30,
- default_description: nil,
base_url: nil
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
privileged_staff: false,
local_bubble: [],
max_frontend_settings_json_chars: 100_000,
- export_prometheus_metrics: true
+ export_prometheus_metrics: true,
+ federated_timeline_available: true
config :pleroma, :welcome,
direct_message: [
primary: %{"name" => "pleroma-fe", "ref" => "stable"},
admin: %{"name" => "admin-fe", "ref" => "stable"},
mastodon: %{"name" => "mastodon-fe", "ref" => "akkoma"},
+ pickable: [
+ "pleroma-fe/stable"
+ ],
swagger: %{
"name" => "swagger-ui",
"ref" => "stable",
private_instance? = :if_instance_is_private
config :pleroma, :restrict_unauthenticated,
- timelines: %{local: private_instance?, federated: private_instance?},
+ timelines: %{local: private_instance?, federated: private_instance?, bubble: true},
profiles: %{local: private_instance?, remote: private_instance?},
activities: %{local: private_instance?, remote: private_instance?}
+hehe, /emoji/hehe.png, Akkoma
+nothehe, /emoji/nothehe.png, Akkoma
key: :export_prometheus_metrics,
type: :boolean,
description: "Enable prometheus metrics (at /api/v1/akkoma/metrics)"
+ },
+ %{
+ key: :federated_timeline_available,
+ type: :boolean,
+ description:
+ "Let people view the 'firehose' feed of all public statuses from all instances."
}
]
},
key: :federated,
type: :boolean,
description: "Disallow viewing the whole known network timeline."
+ },
+ %{
+ key: :bubble,
+ type: :boolean,
+ description: "Disallow viewing the bubble timeline."
}
]
},
description:
"A map containing available frontends and parameters for their installation.",
children: frontend_options
+ },
+ %{
+ key: :pickable,
+ type: {:list, :string},
+ description:
+ "A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable."
}
]
},
config :pleroma, Pleroma.Upload,
filters: [],
- link_name: false,
- default_description: :filename
+ link_name: false
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
* `proxy_remote`: If you're using a remote uploader, Akkoma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
-* `default_description`: Sets which default description an image has if none is set explicitly. Options: nil (default) - Don't set a default, :filename - use the filename of the file, a string (e.g. "attachment") - Use this string
!!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
## Required dependencies
* PostgreSQL 9.6+
-* Elixir 1.12+ (1.13+ recommended)
-* Erlang OTP 22.2+
+* Elixir 1.14+
+* Erlang OTP 24+
* git
* file / libmagic
* gcc (clang might also work)
--- /dev/null
+# Installing on Yunohost
+
+[YunoHost](https://yunohost.org) is a server operating system aimed at self-hosting. The YunoHost community maintains a package of Akkoma which allows you to install Akkoma on YunoHost. You can install it via the normal way through the admin web interface, or through the CLI. More information can be found at [the repo of the package](https://github.com/YunoHost-Apps/akkoma_ynh).
+
+## Questions
+
+Questions and problems related to the YunoHost parts can be done through the [YunoHost channels](https://yunohost.org/en/help).
+
+For questions about Akkoma, check out the [Akkoma community channels](../../#community-channels).
-elixir_version=1.9.4
-erlang_version=22.3.4.1
+elixir_version=1.14.3
+erlang_version=25.3
Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
|> IO.puts()
end
+
+ def run(["notifications", nickname]) do
+ start_pleroma()
+ user = Repo.get_by!(User, nickname: nickname)
+ account_ap_id = user.ap_id
+ options = %{account_ap_id: user.ap_id}
+
+ query =
+ user
+ |> Pleroma.Notification.for_user_query(options)
+ |> where([n, a], a.actor == ^account_ap_id)
+ |> limit(20)
+
+ Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
+ |> IO.puts()
+ end
+
+ def run(["known_network", nickname]) do
+ start_pleroma()
+ user = Repo.get_by!(User, nickname: nickname)
+
+ params =
+ %{}
+ |> Map.put(:type, ["Create"])
+ |> Map.put(:local_only, false)
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:reply_filtering_user, user)
+ # Restricts unfederated content to authenticated users
+ |> Map.put(:includes_local_public, not is_nil(user))
+ |> Map.put(:restrict_unlisted, true)
+
+ query =
+ Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query(
+ [Pleroma.Constants.as_public()],
+ params
+ )
+ |> limit(20)
+
+ Ecto.Adapters.SQL.explain(Repo, :all, query, analyze: true, timeout: :infinity)
+ |> IO.puts()
+ end
end
def get_create_by_object_ap_id_with_object(_), do: nil
+ def get_local_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
+ ap_id
+ |> create_by_object_ap_id()
+ |> where(local: true)
+ |> Repo.one()
+ end
+
@spec create_by_id_with_object(String.t()) :: t() | nil
def create_by_id_with_object(id) do
get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"])
%Instance{
host: Pleroma.Web.Endpoint.host(),
favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
- nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
+ nodeinfo: Pleroma.Web.Nodeinfo.Nodeinfo.get_nodeinfo("2.1")
}
end
}
defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
- defp get_description(opts, upload) do
- case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
- {description, _} when is_binary(description) -> description
- {_, :filename} -> upload.name
- {_, str} when is_binary(str) -> str
- _ -> ""
- end
- end
-
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
- description = get_description(opts, upload),
+ description = Map.get(opts, :description) || "",
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
def invisible?(_), do: false
def avatar_url(user, options \\ []) do
- case user.avatar do
- %{"url" => [%{"href" => href} | _]} ->
- href
-
- _ ->
- unless options[:no_default] do
- Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
- end
- end
+ default = Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
+ do_optional_url(user.avatar, default, options)
end
def banner_url(user, options \\ []) do
- case user.banner do
- %{"url" => [%{"href" => href} | _]} -> href
- _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
+ do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options)
+ end
+
+ defp do_optional_url(field, default, options) do
+ case field do
+ %{"url" => [%{"href" => href} | _]} when is_binary(href) ->
+ href
+
+ _ ->
+ unless options[:no_default], do: default
end
end
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
- with {:ok, data} <- Upload.store(file, opts) do
+ with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
obj_data = Maps.put_if_present(data, "actor", opts[:actor])
Repo.insert(%Object{data: obj_data})
end
end
+ defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do
+ %Plug.Upload{
+ upload
+ | filename: Path.basename(filename)
+ }
+ end
+
+ defp sanitize_upload_file(upload), do: upload
+
@spec get_actor_url(any()) :: binary() | nil
defp get_actor_url(url) when is_binary(url), do: url
defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
- alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
|> json("Invalid HTTP Signature")
end
- # POST /relay/inbox -or- POST /internal/fetch/inbox
- def inbox(conn, %{"type" => "Create"} = params) do
- if FederatingPlug.federating?() do
- post_inbox_relayed_create(conn, params)
- else
- conn
- |> put_status(:bad_request)
- |> json("Not federating")
- end
- end
-
def inbox(conn, _params) do
conn
|> put_status(:bad_request)
|> json("error, missing HTTP Signature")
end
- defp post_inbox_relayed_create(conn, params) do
- Logger.debug(
- "Signature missing or not from author, relayed Create message, fetching object from source"
- )
-
- Fetcher.fetch_object_from_id(params["object"]["id"])
-
- json(conn, "ok")
- end
-
defp represent_service_actor(%User{} = user, conn) do
conn
|> put_resp_content_type("application/activity+json")
Config.get([:mrf_simple, :reject], [])
end
+ defp allowed_instances do
+ Config.get([:mrf_simple, :accept])
+ end
+
def should_federate?(url) do
%{host: host} = URI.parse(url)
- quarantined_instances =
- blocked_instances()
+ with allowed <- allowed_instances(),
+ false <- Enum.empty?(allowed) do
+ allowed
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ |> Pleroma.Web.ActivityPub.MRF.subdomain_match?(host)
+ else
+ _ ->
+ quarantined_instances =
+ blocked_instances()
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
- !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+ not Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
+ end
end
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
alias Pleroma.Akkoma.FrontendSettingsProfile
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+
+ plug(
+ OAuthScopesPlug,
+ @unauthenticated_access
+ when action in [
+ :available_frontends,
+ :update_preferred_frontend
+ ]
+ )
+
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:accounts"]}
|> json(profile.settings)
end
end
+
+ @doc "GET /api/v1/akkoma/preferred_frontend/available"
+ def available_frontends(conn, _params) do
+ available = Pleroma.Config.get([:frontends, :pickable])
+
+ conn
+ |> json(available)
+ end
+
+ @doc "PUT /api/v1/akkoma/preferred_frontend"
+ def update_preferred_frontend(
+ %{body_params: %{frontend_name: preferred_frontend}} = conn,
+ _params
+ ) do
+ conn
+ |> put_resp_cookie("preferred_frontend", preferred_frontend)
+ |> json(%{frontend_name: preferred_frontend})
+ end
end
--- /dev/null
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherController do
+ use Pleroma.Web, :controller
+ alias Pleroma.Config
+
+ @doc "GET /akkoma/frontend"
+ def switch(conn, _params) do
+ pickable = Config.get([:frontends, :pickable], [])
+
+ conn
+ |> put_view(Pleroma.Web.AkkomaAPI.FrontendSwitcherView)
+ |> render("switch.html", choices: pickable)
+ end
+
+ @doc "POST /akkoma/frontend"
+ def do_switch(conn, params) do
+ conn
+ |> put_resp_cookie("preferred_frontend", params["frontend"])
+ |> html("<meta http-equiv=\"refresh\" content=\"0; url=/\">")
+ end
+end
--- /dev/null
+defmodule Pleroma.Web.AkkomaAPI.FrontendSwitcherView do
+ use Pleroma.Web, :view
+end
@spec list_profiles_operation() :: Operation.t()
def list_profiles_operation() do
%Operation{
- tags: ["Retrieve frontend setting profiles"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profiles",
description: "List frontend setting profiles",
operationId: "AkkomaAPI.FrontendSettingsController.list_profiles",
@spec get_profile_operation() :: Operation.t()
def get_profile_operation() do
%Operation{
- tags: ["Retrieve frontend setting profile"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profile",
description: "Get frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.get_profile",
@spec delete_profile_operation() :: Operation.t()
def delete_profile_operation() do
%Operation{
- tags: ["Delete frontend setting profile"],
+ tags: ["Frontends"],
summary: "Delete frontend Settings Profile",
description: "Delete frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.delete_profile",
@spec update_profile_operation() :: Operation.t()
def update_profile_operation() do
%Operation{
- tags: ["Update frontend setting profile"],
+ tags: ["Frontends"],
summary: "Frontend Settings Profile",
description: "Update frontend setting profile",
operationId: "AkkomaAPI.FrontendSettingsController.update_profile_operation",
}
end
+ def available_frontends_operation() do
+ %Operation{
+ tags: ["Frontends"],
+ summary: "Frontend Settings Profiles",
+ description: "List frontend setting profiles",
+ operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+ responses: %{
+ 200 =>
+ Operation.response("Frontends", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string
+ }
+ })
+ }
+ }
+ end
+
+ def update_preferred_frontend_operation() do
+ %Operation{
+ tags: ["Frontends"],
+ summary: "Frontend Settings Profiles",
+ description: "List frontend setting profiles",
+ operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
+ requestBody:
+ request_body(
+ "Frontend",
+ %Schema{
+ type: :object,
+ required: [:frontend_name],
+ properties: %{
+ frontend_name: %Schema{
+ type: :string,
+ description: "Frontend name"
+ }
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 =>
+ Operation.response("Frontends", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string
+ }
+ })
+ }
+ }
+ end
+
def frontend_name_param do
Operation.parameter(:frontend_name, :path, :string, "Frontend name",
example: "pleroma-fe",
operationId: "TimelineController.public",
responses: %{
200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
- 401 => Operation.response("Error", "application/json", ApiError)
+ 401 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def redirector(conn, _params, code \\ 200) do
conn
|> put_resp_content_type("text/html")
- |> send_file(code, index_file_path())
+ |> send_file(code, index_file_path(conn))
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
end
def redirector_with_meta(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
tags = build_tags(conn, params)
preloads = preload_data(conn, params)
end
def redirector_with_preload(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
preloads = preload_data(conn, params)
tags = Metadata.build_static_tags(params)
title = "<title>#{Pleroma.Config.get([:instance, :name])}</title>"
|> text("")
end
- defp index_file_path do
- Pleroma.Web.Plugs.InstanceStatic.file_path("index.html")
+ defp index_file_path(conn) do
+ frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary)
+ Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type)
end
defp build_tags(conn, params) do
alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_public_check when action in [:public, :hashtag])
+ plug(:skip_public_check when action in [:public, :hashtag, :bubble])
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
plug(RateLimiter, [name: :timeline, bucket_name: :bubble_timeline] when action == :bubble)
- plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct, :bubble])
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
- when action in [:public, :hashtag]
+ when action in [:public, :hashtag, :bubble]
)
+ require Logger
+
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
+ %{nickname: nickname} = user
+
+ Logger.debug("TimelineController.home: #{nickname}")
+
followed_hashtags =
user
|> User.followed_hashtags()
|> Map.put(:followed_hashtags, followed_hashtags)
|> Map.delete(:local)
+ Logger.debug("TimelineController.home: #{nickname} - fetching activities")
+
activities =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
+ Logger.debug("TimelineController.home: #{nickname} - rendering")
+
conn
|> add_link_headers(activities)
|> render("index.json",
# GET /api/v1/timelines/direct
def direct(%{assigns: %{user: user}} = conn, params) do
+ Logger.debug("TimelineController.direct: #{user.nickname}")
+
params =
params
|> Map.put(:type, "Create")
|> Map.put(:user, user)
|> Map.put(:visibility, "direct")
+ Logger.debug("TimelineController.direct: #{user.nickname} - fetching activities")
+
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
+ Logger.debug("TimelineController.direct: #{user.nickname} - rendering")
+
conn
|> add_link_headers(activities)
|> render("index.json",
)
end
- defp restrict_unauthenticated?(true = _local_only) do
- Config.restrict_unauthenticated_access?(:timelines, :local)
- end
-
- defp restrict_unauthenticated?(_) do
- Config.restrict_unauthenticated_access?(:timelines, :federated)
+ defp restrict_unauthenticated?(type) do
+ Config.restrict_unauthenticated_access?(:timelines, type)
end
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
+ Logger.debug("TimelineController.public")
local_only = params[:local]
+ timeline_type = if local_only, do: :local, else: :federated
+
+ with {:enabled, true} <-
+ {:enabled, local_only || Config.get([:instance, :federated_timeline_available], true)},
+ {:authenticated, true} <-
+ {:authenticated, !(is_nil(user) and restrict_unauthenticated?(timeline_type))} do
+ Logger.debug("TimelineController.public: fetching activities")
- if is_nil(user) and restrict_unauthenticated?(local_only) do
- fail_on_bad_auth(conn)
- else
activities =
params
|> Map.put(:type, ["Create"])
|> Map.put(:includes_local_public, not is_nil(user))
|> ActivityPub.fetch_public_activities()
+ Logger.debug("TimelineController.public: rendering")
+
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json",
as: :activity,
with_muted: Map.get(params, :with_muted, false)
)
+ else
+ {:enabled, false} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Federated timeline is disabled"})
+
+ {:authenticated, false} ->
+ fail_on_bad_auth(conn)
end
end
# GET /api/v1/timelines/bubble
def bubble(%{assigns: %{user: user}} = conn, params) do
- bubble_instances =
- Enum.uniq(
- Config.get([:instance, :local_bubble], []) ++
- [Pleroma.Web.Endpoint.host()]
- )
+ Logger.debug("TimelineController.bubble")
- if is_nil(user) do
+ if is_nil(user) and restrict_unauthenticated?(:bubble) do
fail_on_bad_auth(conn)
else
+ bubble_instances =
+ Enum.uniq(
+ Config.get([:instance, :local_bubble], []) ++
+ [Pleroma.Web.Endpoint.host()]
+ )
+
+ Logger.debug("TimelineController.bubble: fetching activities")
+
activities =
params
|> Map.put(:type, ["Create"])
|> Map.put(:instance, bubble_instances)
|> ActivityPub.fetch_public_activities()
+ Logger.debug("TimelineController.bubble: rendering")
+
conn
|> add_link_headers(activities)
|> render("index.json",
def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local]
- if is_nil(user) and restrict_unauthenticated?(local_only) do
+ if is_nil(user) and restrict_unauthenticated?(if local_only, do: :local, else: :federated) do
fail_on_bad_auth(conn)
else
activities = hashtag_fetching(params, user, local_only)
"pleroma:api/v1/notifications:include_types_filter",
"quote_posting",
"editing",
+ if !Enum.empty?(Config.get([:instance, :local_bubble], [])) do
+ "bubble_timeline"
+ end,
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.EmojiReactionController
+ require Logger
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
defp reblogged?(_activity, _user), do: false
def render("index.json", opts) do
+ Logger.debug("Rendering index")
reading_user = opts[:for]
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
activities = Enum.filter(opts.activities, & &1)
def render(
"show.json",
- %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
+ %{activity: %{id: id, data: %{"type" => "Announce", "object" => _object}} = activity} =
+ opts
) do
+ Logger.debug("Rendering reblog #{id}")
user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
object = Object.normalize(activity, fetch: false)
}
end
- def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ def render("show.json", %{activity: %{id: id, data: %{"object" => _object}} = activity} = opts) do
+ Logger.debug("Rendering status #{id}")
+
with %Object{} = object <- Object.normalize(activity, fetch: false) do
user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
end
def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ Logger.debug("Rendering history for #{activity.id}")
object = Object.normalize(activity, fetch: false)
hashtags = Object.hashtags(object)
def render("attachment_meta.json", _), do: nil
def render("context.json", %{activity: activity, activities: activities, user: user}) do
+ Logger.debug("Rendering context for #{activity.id}")
+
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
privilegedStaff: Config.get([:instance, :privileged_staff]),
- localBubbleInstances: Config.get([:instance, :local_bubble], [])
+ localBubbleInstances: Config.get([:instance, :local_bubble], []),
+ publicTimelineVisibility: %{
+ federated:
+ !Config.restrict_unauthenticated_access?(:timelines, :federated) &&
+ Config.get([:instance, :federated_timeline_available], true),
+ local: !Config.restrict_unauthenticated_access?(:timelines, :local),
+ bubble: !Config.restrict_unauthenticated_access?(:timelines, :bubble)
+ },
+ federatedTimelineAvailable: Config.get([:instance, :federated_timeline_available], true)
}
}
end
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
- alias Pleroma.Config
- alias Pleroma.Stats
- alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
- alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Nodeinfo.Nodeinfo
def schemas(conn, _params) do
response = %{
json(conn, response)
end
- # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
- # under software.
- def raw_nodeinfo do
- stats = Stats.get_stats()
-
- staff_accounts =
- User.all_superusers()
- |> Enum.map(fn u -> u.ap_id end)
- |> Enum.filter(fn u -> not Enum.member?(Config.get([:instance, :staff_transparency]), u) end)
-
- features = InstanceView.features()
- federation = InstanceView.federation()
-
- %{
- version: "2.0",
- software: %{
- name: Pleroma.Application.name() |> String.downcase(),
- version: Pleroma.Application.version()
- },
- protocols: Publisher.gather_nodeinfo_protocol_names(),
- services: %{
- inbound: [],
- outbound: []
- },
- openRegistrations: Config.get([:instance, :registrations_open]),
- usage: %{
- users: %{
- total: Map.get(stats, :user_count, 0)
- },
- localPosts: Map.get(stats, :status_count, 0)
- },
- metadata: %{
- nodeName: Config.get([:instance, :name]),
- nodeDescription: Config.get([:instance, :description]),
- private: !Config.get([:instance, :public], true),
- suggestions: %{
- enabled: false
- },
- staffAccounts: staff_accounts,
- federation: federation,
- pollLimits: Config.get([:instance, :poll_limits]),
- postFormats: Config.get([:instance, :allowed_post_formats]),
- uploadLimits: %{
- general: Config.get([:instance, :upload_limit]),
- avatar: Config.get([:instance, :avatar_upload_limit]),
- banner: Config.get([:instance, :banner_upload_limit]),
- background: Config.get([:instance, :background_upload_limit])
- },
- fieldsLimits: %{
- maxFields: Config.get([:instance, :max_account_fields]),
- maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
- nameLength: Config.get([:instance, :account_field_name_length]),
- valueLength: Config.get([:instance, :account_field_value_length])
- },
- accountActivationRequired: Config.get([:instance, :account_activation_required], false),
- invitesEnabled: Config.get([:instance, :invites_enabled], false),
- mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
- features: features,
- restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
- skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
- localBubbleInstances: Config.get([:instance, :local_bubble], [])
- }
- }
- end
-
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
# and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
- def nodeinfo(conn, %{"version" => "2.0"}) do
+ def nodeinfo(conn, %{"version" => version}) when version in ["2.0", "2.1"] do
conn
|> put_resp_header(
"content-type",
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
)
- |> json(raw_nodeinfo())
- end
-
- def nodeinfo(conn, %{"version" => "2.1"}) do
- raw_response = raw_nodeinfo()
-
- updated_software =
- raw_response
- |> Map.get(:software)
- |> Map.put(:repository, Pleroma.Application.repository())
-
- response =
- raw_response
- |> Map.put(:software, updated_software)
- |> Map.put(:version, "2.1")
-
- conn
- |> put_resp_header(
- "content-type",
- "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
- )
- |> json(response)
+ |> json(Nodeinfo.get_nodeinfo(version))
end
def nodeinfo(conn, _) do
def object(conn, _params) do
with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <-
- {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
+ {:activity, Activity.get_local_create_by_object_ap_id(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
{_, false} <- {:local_public?, Visibility.is_local_public?(activity)} do
redirect(conn, to: "/notice/#{activity.id}")
defmodule Pleroma.Web.Plugs.FrontendStatic do
require Pleroma.Constants
+ @frontend_cookie_name "preferred_frontend"
+
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
"""
@behaviour Plug
- def file_path(path, frontend_type \\ :primary) do
- if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
- instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ defp instance_static_path do
+ Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ end
+
+ def file_path(path, frontend_type \\ :primary)
+ def file_path(path, frontend_type) when is_atom(frontend_type) do
+ if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
Path.join([
- instance_static_path,
+ instance_static_path(),
"frontends",
configuration["name"],
configuration["ref"],
end
end
+ def file_path(path, frontend_type) when is_binary(frontend_type) do
+ Path.join([
+ instance_static_path(),
+ "frontends",
+ frontend_type,
+ path
+ ])
+ end
+
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
with false <- api_route?(conn.path_info),
false <- invalid_path?(conn.path_info),
true <- enabled?(opts[:if]),
- frontend_type <- Map.get(opts, :frontend_type, :primary),
+ fallback_frontend_type <- Map.get(opts, :frontend_type, :primary),
+ frontend_type <- preferred_or_fallback(conn, fallback_frontend_type),
path when not is_nil(path) <- file_path("", frontend_type) do
call_static(conn, opts, path)
else
end
end
+ def preferred_frontend(conn) do
+ %{req_cookies: cookies} =
+ conn
+ |> Plug.Conn.fetch_cookies()
+
+ Map.get(cookies, @frontend_cookie_name)
+ end
+
+ # Only override primary frontend
+ def preferred_or_fallback(conn, :primary) do
+ case preferred_frontend(conn) do
+ nil ->
+ :primary
+
+ frontend ->
+ if Enum.member?(Pleroma.Config.get([:frontends, :pickable], []), frontend) do
+ frontend
+ else
+ :primary
+ end
+ end
+ end
+
+ def preferred_or_fallback(_conn, fallback), do: fallback
+
defp enabled?(if_opt) when is_function(if_opt), do: if_opt.()
defp enabled?(true), do: true
defp enabled?(_), do: false
require Logger
+ @mix_env Mix.env()
+
def init(opts), do: opts
def call(conn, _options) do
style_src = "style-src 'self' '#{nonce_tag}'"
font_src = "font-src 'self'"
- script_src = "script-src 'self' '#{nonce_tag}'"
+ script_src = "script-src 'self' '#{nonce_tag}' "
+
+ script_src =
+ if @mix_env == :dev do
+ "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
+ else
+ script_src
+ end
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
insecure = if scheme == "https", do: "upgrade-insecure-requests"
"""
@behaviour Plug
- def file_path(path) do
+ def file_path(path, frontend_type \\ :primary) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
- frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary)
+ frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, frontend_type)
(File.exists?(instance_path) && instance_path) ||
(frontend_path && File.exists?(frontend_path) && frontend_path) ||
put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
end
+ scope "/akkoma/", Pleroma.Web.AkkomaAPI do
+ pipe_through(:browser)
+
+ get("/frontend", FrontendSwitcherController, :switch)
+ post("/frontend", FrontendSwitcherController, :do_switch)
+ end
+
+ scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
+ pipe_through(:api)
+
+ get(
+ "/api/v1/akkoma/preferred_frontend/available",
+ FrontendSettingsController,
+ :available_frontends
+ )
+
+ put(
+ "/api/v1/akkoma/preferred_frontend",
+ FrontendSettingsController,
+ :update_preferred_frontend
+ )
+ end
+
scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
pipe_through(:authenticated_api)
get("/metrics", MetricsController, :show)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
- get("/timelines/bubble", TimelineController, :bubble)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
+ get("/timelines/bubble", TimelineController, :bubble)
get("/polls/:id", PollController, :show)
--- /dev/null
+<h2>Switch Frontend</h2>
+
+<h3>After you submit, you will need to refresh manually to get your new frontend!</h3>
+
+<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %>
+ <%= select(f, :frontend, @choices) %>
+
+ <%= submit do: "submit" %>
+<% end %>
+
def project do
[
app: :pleroma,
- version: version("3.7.1"),
- elixir: "~> 1.12",
+ version: version("3.7.2"),
+ elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
"earmark": {:hex, :earmark, "1.4.37", "56ce845c543393aa3f9b294c818c3d783452a4a67e4ab18c4303a954a8b59363", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d86d5e12868db86d5321b00e62a4bbcb4150346e4acc9a90a041fb188a5cb106"},
"earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
- "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
+ "ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"},
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
- "ex_doc": {:hex, :ex_doc, "0.29.2", "dfa97532ba66910b2a3016a4bbd796f41a86fc71dd5227e96f4c8581fdf0fdf0", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6b5d7139eda18a753e3250e27e4a929f8d2c880dd0d460cb9986305dea3e03af"},
+ "ex_doc": {:hex, :ex_doc, "0.29.3", "f07444bcafb302db86e4f02d8bbcd82f2e881a0dcf4f3e4740e4b8128b9353f7", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3dc6787d7b08801ec3b51e9bd26be5e8826fbf1a17e92d1ebc252e1a1c75bfe1"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.17", "74938b02f3c531bed3f87fe1ea39af6b5b2d26ab1405e77e76b8ef5df9ffa8a1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4b5710e19a29b8dc93b7af4bab4739c067a3cb759af01ffc3057165453dce38"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.4", "615f8f393135de7e0cbb4bd00ba238b1e0cd324b0d90efbaee613c2f02ca5e5c", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "3971221846232021ab5e3c7489fd62ec5bfd6a2e01cae10a317ccf6fb350571c"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
- "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.4", "34c380ef387cc7e8d537854ddd4b7096c79a4397d53587cb80419c782b03fdbc", [:mix], [], "hexpm", "4de415f03faec94d9da9be8c12cb51e9c98cbf66d732b6df669d4562d8e91acc"},
+ "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
+ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
- "timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
+ "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
assert result ==
%{
"id" => result["id"],
- "name" => "image.jpg",
+ "name" => "",
"type" => "Document",
"mediaType" => "image/jpeg",
"url" => [
"e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
end
- test "copies the file to the configured folder without deduping" do
- File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
-
- file = %Plug.Upload{
- content_type: "image/jpeg",
- path: Path.absname("test/fixtures/image_tmp.jpg"),
- filename: "an [image.jpg"
- }
-
- {:ok, data} = Upload.store(file)
- assert data["name"] == "an [image.jpg"
- end
-
test "fixes incorrect content type when base64 is given" do
params = %{
img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}"
}
{:ok, data} = Upload.store(params)
- assert String.ends_with?(data["name"], ".jpg")
+ assert String.ends_with?(List.first(data["url"])["href"], ".jpg")
end
test "copies the file to the configured folder with anonymizing filename" do
assert User.avatar_url(user, no_default: true) == nil
end
+ test "avatar object with nil in href" do
+ user = insert(:user, avatar: %{"url" => [%{"href" => nil}]})
+ assert User.avatar_url(user) != nil
+ end
+
+ test "banner object with nil in href" do
+ user = insert(:user, banner: %{"url" => [%{"href" => nil}]})
+ assert User.banner_url(user) != nil
+ end
+
test "get_host/1" do
user = insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain")
assert User.get_host(user) == "lain.com"
assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
end
- @tag capture_log: true
- test "without valid signature, " <>
- "it only accepts Create activities and requires enabled federation",
- %{conn: conn} do
- data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
- non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
-
- conn = put_req_header(conn, "content-type", "application/activity+json")
-
- clear_config([:instance, :federating], false)
-
- conn
- |> post("/inbox", data)
- |> json_response(403)
-
- conn
- |> post("/inbox", non_create_data)
- |> json_response(403)
-
- clear_config([:instance, :federating], true)
-
- ret_conn = post(conn, "/inbox", data)
- assert "ok" == json_response(ret_conn, 200)
-
- conn
- |> post("/inbox", non_create_data)
- |> json_response(400)
- end
-
test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
%{test_file: test_file}
end
- test "sets a description if given", %{test_file: file} do
- {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
- assert object.data["name"] == "a cool file"
- end
-
- test "it sets the default description depending on the configuration", %{test_file: file} do
- clear_config([Pleroma.Upload, :default_description])
-
- clear_config([Pleroma.Upload, :default_description], nil)
- {:ok, %Object{} = object} = ActivityPub.upload(file)
- assert object.data["name"] == ""
-
- clear_config([Pleroma.Upload, :default_description], :filename)
- {:ok, %Object{} = object} = ActivityPub.upload(file)
- assert object.data["name"] == "an_image.jpg"
-
- clear_config([Pleroma.Upload, :default_description], "unnamed attachment")
+ test "strips / from filename", %{test_file: file} do
+ file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
{:ok, %Object{} = object} = ActivityPub.upload(file)
- assert object.data["name"] == "unnamed attachment"
+ [%{"href" => href}] = object.data["url"]
+ assert Regex.match?(~r"/bad.jpg$", href)
+ refute Regex.match?(~r"/nested/", href)
end
- test "copies the file to the configured folder", %{test_file: file} do
- clear_config([Pleroma.Upload, :default_description], :filename)
- {:ok, %Object{} = object} = ActivityPub.upload(file)
- assert object.data["name"] == "an_image.jpg"
+ test "sets a description if given", %{test_file: file} do
+ {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file")
+ assert object.data["name"] == "a cool file"
end
test "works with base64 encoded images" do
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
+
+ test "Do not allow nested filename", %{conn: conn, image: image} do
+ image = %Plug.Upload{
+ image
+ | filename: "../../../../../nested/file.jpg"
+ }
+
+ desc = "Description of the image"
+
+ media =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/v1/media", %{"file" => image, "description" => desc})
+ |> json_response_and_validate_schema(:ok)
+
+ refute Regex.match?(~r"/nested/", media["url"])
+ end
end
describe "Update media description" do
assert [] = result
end
+
+ test "should return 404 if disabled" do
+ clear_config([:instance, :federated_timeline_available], false)
+
+ result =
+ build_conn()
+ |> get("/api/v1/timelines/public")
+ |> json_response_and_validate_schema(404)
+
+ assert %{"error" => "Federated timeline is disabled"} = result
+ end
+
+ test "should not return 404 if local is specified" do
+ clear_config([:instance, :federated_timeline_available], false)
+
+ build_conn()
+ |> get("/api/v1/timelines/public?local=true")
+ |> json_response_and_validate_schema(200)
+ end
end
defp local_and_remote_activities do
end
describe "bubble" do
- setup do: oauth_access(["read:statuses"])
-
- test "filtering", %{conn: conn, user: user} do
+ test "filtering" do
+ %{conn: conn, user: user} = oauth_access(["read:statuses"])
clear_config([:instance, :local_bubble], [])
# our endpoint host has a port in it so let's set the AP ID
local_user = insert(:user, %{ap_id: "https://localhost/users/user"})
assert local_activity.id in one_instance
- # If we have others, also include theirs
+ # If we have others, also include theirs
clear_config([:instance, :local_bubble], ["example.com"])
two_instances =
assert local_activity.id in two_instances
assert remote_activity.id in two_instances
end
+
+ test "restrict_unauthenticated with bubble timeline", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines, :bubble], true)
+
+ conn
+ |> get("/api/v1/timelines/bubble")
+ |> json_response_and_validate_schema(:unauthorized)
+
+ clear_config([:restrict_unauthenticated, :timelines, :bubble], false)
+
+ conn
+ |> get("/api/v1/timelines/bubble")
+ |> json_response_and_validate_schema(200)
+ end
end
defp create_remote_activity(user) do
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
+ test "Strip / from upload files", %{user: user, conn: conn} do
+ new_image = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "../../../../nested/an_image.jpg"
+ }
+
+ assert user.avatar == %{}
+
+ res =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "avatar" => new_image,
+ "header" => new_image,
+ "pleroma_background_image" => new_image
+ })
+
+ assert user_response = json_response_and_validate_schema(res, 200)
+ assert user_response["avatar"]
+ assert user_response["header"]
+ assert user_response["pleroma"]["background_image"]
+ refute Regex.match?(~r"/nested/", user_response["avatar"])
+ refute Regex.match?(~r"/nested/", user_response["header"])
+ refute Regex.match?(~r"/nested/", user_response["pleroma"]["background_image"])
+
+ user = User.get_by_id(user.id)
+ refute user.avatar == %{}
+ end
+
test "requires 'write:accounts' permission" do
token1 = insert(:oauth_token, scopes: ["read"])
token2 = insert(:oauth_token, scopes: ["write", "follow"])
}
with_mock(
- Pleroma.Web.Nodeinfo.NodeinfoController,
- raw_nodeinfo: fn -> %{version: "2.0"} end
+ Pleroma.Web.Nodeinfo.Nodeinfo,
+ get_nodeinfo: fn _ -> %{version: "2.0"} end
) do
assert expected ==
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
assert response["metadata"]["federation"]["mrf_simple_info"] == expected_config
end
end
+
+ describe "public timeline visibility" do
+ test "shows public timeline visibility", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: false})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+ clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: false})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == false
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+ clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: true})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == false
+ end
+ end
end
"main",
"ostatus_subscribe",
"oauth",
+ "akkoma",
"objects",
"activities",
"notice",
assert %{valid_signature: false} == conn.assigns
end
+ test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ clear_config([:mrf_simple, :accept], [
+ {"mastodon.example.org", "anime is allowed"}
+ ])
+
+ on_exit(fn ->
+ Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+ Pleroma.Config.put([:mrf_simple, :accept], [])
+ end)
+
+ conn =
+ build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+ |> set_signature("http://mastodon.example.org/users/admin")
+ |> MappedSignatureToIdentityPlug.call(%{})
+
+ assert conn.assigns[:valid_signature]
+ refute is_nil(conn.assigns.user)
+ end
+
+ test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ clear_config([:mrf_simple, :accept], [
+ {"misskey.example.org", "anime is allowed"}
+ ])
+
+ on_exit(fn ->
+ Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false)
+ Pleroma.Config.put([:mrf_simple, :accept], [])
+ end)
+
+ conn =
+ build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
+ |> set_signature("http://mastodon.example.org/users/admin")
+ |> MappedSignatureToIdentityPlug.call(%{})
+
+ assert %{valid_signature: false} == conn.assigns
+ end
+
@tag skip: "known breakage; the testsuite presently depends on it"
test "it considers a mapped identity to be invalid when the identity cannot be found" do
conn =