Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85649429
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
160 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/config/config.exs b/config/config.exs
index e8aad855c..45034a775 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,483 +1,488 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter]
config :pleroma, Pleroma.Captcha,
enabled: false,
seconds_valid: 60,
method: Pleroma.Captcha.Kocaptcha
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: true,
proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
public_endpoint: "https://s3.amazonaws.com"
config :pleroma, Pleroma.Uploaders.MDII,
cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi",
files: "https://mdii.sakura.ne.jp"
config :pleroma, :emoji,
shortcode_globs: ["/emoji/custom/**/*.png"],
groups: [
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
config :logger, :ex_syslogger,
level: :debug,
ident: "Pleroma",
format: "$metadata[$level] $message",
metadata: [:request_id]
config :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
config :pleroma, :websub, Pleroma.Web.Websub
config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, Pleroma.HTTP
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
adapter: [
ssl_options: [
# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
]
]
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
registrations_open: true,
federating: true,
federation_reachability_timeout_days: 7,
+ federation_publisher_modules: [
+ Pleroma.Web.ActivityPub.Publisher,
+ Pleroma.Web.Websub,
+ Pleroma.Web.Salmon
+ ],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
mrf_transparency: true,
autofollowed_nicknames: [],
max_pinned_statuses: 1,
no_attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false
config :pleroma, :app_account_creation, enabled: false, max_requests: 5, interval: 1800
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
allow_inline_images: true,
allow_headings: false,
allow_tables: false,
allow_fonts: false,
scrub_policy: [
Pleroma.HTML.Transform.MediaProxy,
Pleroma.HTML.Scrubber.Default
]
# Deprecated, will be gone in 1.0
config :pleroma, :fe,
theme: "pleroma-dark",
logo: "/static/logo.png",
logo_mask: true,
logo_margin: "0.1em",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
formatting_options_enabled: false,
collapse_message_with_subject: false,
hide_post_stats: false,
hide_user_stats: false,
scope_copy: true,
subject_line_behavior: "email",
always_show_subject_input: true
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true
},
masto_fe: %{
showInstanceSpecificPanel: true
}
config :pleroma, :activitypub,
accept_blocks: true,
unfollow_blocked: true,
outgoing_blocks: true,
follow_handshake_timeout: 500
config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_normalize_markup, scrub_policy: Pleroma.HTML.Scrubber.Default
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
reject: [],
accept: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :rich_media, enabled: true
config :pleroma, :media_proxy,
enabled: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
whitelist: []
config :pleroma, :chat, enabled: true
config :phoenix, :format_encoders, json: Jason
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, Pleroma.Web.Metadata,
providers: [Pleroma.Web.Metadata.Providers.RelMe],
unfurl_nsfw: false
config :pleroma, :suggestions,
enabled: false,
third_party_engine:
"http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
timeout: 300_000,
limit: 23,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin"
config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"users",
"web"
]
config :pleroma, Pleroma.Web.Federator.RetryQueue,
enabled: false,
max_jobs: 20,
initial_timeout: 30,
max_retries: 5
config :pleroma_job_queue, :queues,
federator_incoming: 50,
federator_outgoing: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
background: 5
config :pleroma, :fetch_initial_posts,
enabled: false,
pages: 5
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
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"
config :esshd,
enabled: false
oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "")
ueberauth_providers =
for strategy <- oauth_consumer_strategies do
strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}"
strategy_module = String.to_atom(strategy_module_name)
{String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}}
end
config :ueberauth,
Ueberauth,
base_path: "/oauth",
providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail
config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 300,
enabled: true
config :pleroma, :oauth2,
token_expires_in: 600,
issue_new_refresh_token: true
# 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"
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 8f8c23a9b..11777c220 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1064 +1,977 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Conversation
- alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
- alias Pleroma.Web.Federator
alias Pleroma.Web.WebFinger
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
- @httpoison Application.get_env(:pleroma, :httpoison)
-
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = data["to"] || []
cc = data["cc"] || []
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
(to ++ cc)
|> Enum.filter(fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil ->
true
user ->
User.following?(user, actor)
end
end)
{recipients, to, cc}
end
defp get_recipients(%{"type" => "Create"} = data) do
to = data["to"] || []
cc = data["cc"] || []
actor = data["actor"] || []
recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = data["to"] || []
cc = data["cc"] || []
recipients = to ++ cc
{recipients, to, cc}
end
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
false <- user.info.deactivated do
:ok
else
_e -> :reject
end
else
:ok
end
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
def increase_replies_count_if_reply(_create_data), do: :noop
def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Object.decrease_replies_count(reply_ap_id)
end
end
def decrease_replies_count_if_reply(_object), do: :noop
def insert(map, local \\ true, fake \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
:ok <- check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
})
# Splice in the child object if we have one.
activity =
if !is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
Notification.create_notifications(activity)
participations =
activity
|> Conversation.create_or_bump_for()
|> get_participations()
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
id: "pleroma:fakeid"
}
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
end
end
defp get_participations({:ok, %{participations: participations}}), do: participations
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Enum.each(participations, fn participation ->
Pleroma.Web.Streamer.stream("participation", participation)
end)
end
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.data["type"] in ["Create"] do
object = Object.normalize(activity)
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:fake, true, activity} ->
{:ok, activity}
end
end
def accept(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity, object}
error -> {:error, error}
end
end
def unlike(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
_e -> {:ok, object}
end
end
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with true <- is_public?(object),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error -> {:error, error}
end
end
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with {:ok, object, activity} <- Object.delete(object),
data <- %{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => activity && activity.id
},
{:ok, activity} <- insert(data, local),
_ <- decrease_replies_count_if_reply(object),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
ap_config = Application.get_env(:pleroma, :activitypub)
unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked)
outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
with true <- unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
_e -> {:ok, nil}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def flag(
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
} = params
) do
# 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
}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
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 ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
end
end
defp fetch_activities_for_context_query(context, opts) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
)
|> order_by([activity], desc: activity.id)
end
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Activity.with_preloaded_object()
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
query =
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
|> Map.put("pinned_activity_ids", user.info.pinned_activities)
recipients =
if reading_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[reading_user.ap_id | reading_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end
fetch_activities(recipients, params)
|> Enum.reverse()
end
defp restrict_since(query, %{"since_id" => ""}), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end
defp restrict_tag(query, _), do: query
defp restrict_to_cc(query, recipients_to, recipients_cc) do
from(
activity in query,
where:
fragment(
"(?->'to' \\?| ?) or (?->'cc' \\?| ?)",
activity.data,
^recipients_to,
activity.data,
^recipients_cc
)
)
end
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{"type" => type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
activity in query,
where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment("?->'object'->>'inReplyTo' is null", activity.data)
)
end
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
mutes = info.mutes
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^["https://www.w3.org/ns/activitystreams#Public"]
)
)
end
defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
from(activity in query, where: activity.id in ^ids)
end
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
muted_reblogs = info.muted_reblogs || []
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity)
base_query
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
obj_data =
if opts[:actor] do
Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data})
end
end
def user_data_from_user_object(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
ap_id: data["id"],
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner,
"locked" => locked
},
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
bio: data["summary"]
}
# nickname can be nil because of virtual actors
user_data =
if data["preferredUsername"] do
Map.put(
user_data,
:nickname,
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
)
else
Map.put(user_data, :nickname, nil)
end
{:ok, user_data}
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
end
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
- def should_federate?(inbox, public) do
- if public do
- true
- else
- inbox_info = URI.parse(inbox)
- !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
- end
- end
-
- def publish(actor, activity) do
- remote_followers =
- if actor.follower_address in activity.recipients do
- {:ok, followers} = User.get_followers(actor)
- followers |> Enum.filter(&(!&1.local))
- else
- []
- end
-
- public = is_public?(activity)
-
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
- json = Jason.encode!(data)
-
- (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
- |> Enum.filter(fn user -> User.ap_enabled?(user) end)
- |> Enum.map(fn %{info: %{source_data: data}} ->
- (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
- end)
- |> Enum.uniq()
- |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
- |> Instances.filter_reachable()
- |> Enum.each(fn {inbox, unreachable_since} ->
- Federator.publish_single_ap(%{
- inbox: inbox,
- json: json,
- actor: actor,
- id: activity.data["id"],
- unreachable_since: unreachable_since
- })
- end)
- end
-
- def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
- Logger.info("Federating #{id} to #{inbox}")
- host = URI.parse(inbox).host
-
- digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
-
- date =
- NaiveDateTime.utc_now()
- |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
-
- signature =
- Pleroma.Web.HTTPSignatures.sign(actor, %{
- host: host,
- "content-length": byte_size(json),
- digest: digest,
- date: date
- })
-
- with {:ok, %{status: code}} when code in 200..299 <-
- result =
- @httpoison.post(
- inbox,
- json,
- [
- {"Content-Type", "application/activity+json"},
- {"Date", date},
- {"signature", signature},
- {"digest", digest}
- ]
- ) do
- if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
- do: Instances.set_reachable(inbox)
-
- result
- else
- {_post_result, response} ->
- unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
- {:error, response}
- end
- end
-
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
# do post-processing on a timeline
def contain_timeline(timeline, user) do
timeline
|> Enum.filter(fn activity ->
contain_activity(activity, user)
end)
end
end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
new file mode 100644
index 000000000..8e3af0a81
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -0,0 +1,152 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Publisher do
+ alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.Instances
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ import Pleroma.Web.ActivityPub.Visibility
+
+ @behaviour Pleroma.Web.Federator.Publisher
+
+ require Logger
+
+ @httpoison Application.get_env(:pleroma, :httpoison)
+
+ @moduledoc """
+ ActivityPub outgoing federation module.
+ """
+
+ @doc """
+ Determine if an activity can be represented by running it through Transmogrifier.
+ """
+ def is_representable?(%Activity{} = activity) do
+ with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
+ true
+ else
+ _e ->
+ false
+ end
+ end
+
+ @doc """
+ Publish a single message to a peer. Takes a struct with the following
+ parameters set:
+
+ * `inbox`: the inbox to publish to
+ * `json`: the JSON message body representing the ActivityPub message
+ * `actor`: the actor which is signing the message
+ * `id`: the ActivityStreams URI of the message
+ """
+ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
+ Logger.info("Federating #{id} to #{inbox}")
+ host = URI.parse(inbox).host
+
+ digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
+
+ date =
+ NaiveDateTime.utc_now()
+ |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
+
+ signature =
+ Pleroma.Web.HTTPSignatures.sign(actor, %{
+ host: host,
+ "content-length": byte_size(json),
+ digest: digest,
+ date: date
+ })
+
+ with {:ok, %{status: code}} when code in 200..299 <-
+ result =
+ @httpoison.post(
+ inbox,
+ json,
+ [
+ {"Content-Type", "application/activity+json"},
+ {"Date", date},
+ {"signature", signature},
+ {"digest", digest}
+ ]
+ ) do
+ if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
+ do: Instances.set_reachable(inbox)
+
+ result
+ else
+ {_post_result, response} ->
+ unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
+ {:error, response}
+ end
+ end
+
+ defp should_federate?(inbox, public) do
+ if public do
+ true
+ else
+ inbox_info = URI.parse(inbox)
+ !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
+ end
+ end
+
+ @doc """
+ Publishes an activity to all relevant peers.
+ """
+ def publish(%User{} = actor, %Activity{} = activity) do
+ remote_followers =
+ if actor.follower_address in activity.recipients do
+ {:ok, followers} = User.get_followers(actor)
+ followers |> Enum.filter(&(!&1.local))
+ else
+ []
+ end
+
+ public = is_public?(activity)
+
+ if public && Config.get([:instance, :allow_relay]) do
+ Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
+ Relay.publish(activity)
+ end
+
+ {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ json = Jason.encode!(data)
+
+ (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
+ |> Enum.filter(fn user -> User.ap_enabled?(user) end)
+ |> Enum.map(fn %{info: %{source_data: data}} ->
+ (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+ end)
+ |> Enum.uniq()
+ |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+ |> Instances.filter_reachable()
+ |> Enum.each(fn {inbox, unreachable_since} ->
+ Pleroma.Web.Federator.Publisher.enqueue_one(
+ __MODULE__,
+ %{
+ inbox: inbox,
+ json: json,
+ actor: actor,
+ id: activity.data["id"],
+ unreachable_since: unreachable_since
+ }
+ )
+ end)
+ end
+
+ def gather_webfinger_links(%User{} = user) do
+ [
+ %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
+ %{
+ "rel" => "self",
+ "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
+ "href" => user.ap_id
+ }
+ ]
+ end
+
+ def gather_nodeinfo_protocol_names, do: ["activitypub"]
+end
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 29e178ba9..8621eda95 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -1,197 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
require Logger
@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
def init do
# 1 minute
Process.sleep(1000 * 60)
refresh_subscriptions()
end
# Client API
def incoming_doc(doc) do
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
end
def incoming_ap_doc(params) do
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
end
def publish(activity, priority \\ 1) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end
- def publish_single_ap(params) do
- PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
- end
-
- def publish_single_websub(websub) do
- PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
- end
-
def verify_websub(websub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end
def request_subscription(sub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
end
def refresh_subscriptions do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end
- def publish_single_salmon(params) do
- PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
- end
-
# Job Worker Callbacks
def perform(:refresh_subscriptions) do
Logger.debug("Federator running refresh subscriptions")
Websub.refresh_subscriptions()
spawn(fn ->
# 6 hours
Process.sleep(1000 * 60 * 60 * 6)
refresh_subscriptions()
end)
end
def perform(:request_subscription, websub) do
Logger.debug("Refreshing #{websub.topic}")
with {:ok, websub} <- Websub.request_subscription(websub) do
Logger.debug("Successfully refreshed #{websub.topic}")
else
_e -> Logger.debug("Couldn't refresh #{websub.topic}")
end
end
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor)
- if Visibility.is_public?(activity) do
- if OStatus.is_representable?(activity) do
- Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
- Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
-
- Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
- Pleroma.Web.Salmon.publish(actor, activity)
- end
-
- if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
- Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
- Relay.publish(activity)
- end
- end
-
- Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
- Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
+ Publisher.publish(actor, activity)
end
end
def perform(:verify_websub, websub) do
Logger.debug(fn ->
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
end)
@websub.verify(websub)
end
def perform(:incoming_doc, doc) do
Logger.info("Got document, trying to parse")
@ostatus.handle_incoming(doc)
end
def perform(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity")
params = Utils.normalize_params(params)
# NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
:ok <- Containment.contain_origin_from_id(params["actor"], params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
%Activity{} ->
Logger.info("Already had #{params["id"]}")
:error
_e ->
# Just drop those for now
Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, pretty: 2))
:error
end
end
- def perform(:publish_single_salmon, params) do
- Salmon.send_to_user(params)
- end
-
- def perform(:publish_single_ap, params) do
- case ActivityPub.publish_one(params) do
- {:ok, _} ->
- :ok
-
- {:error, _} ->
- RetryQueue.enqueue(params, ActivityPub)
- end
- end
-
def perform(
:publish_single_websub,
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
) do
case Websub.publish_one(params) do
{:ok, _} ->
:ok
{:error, _} ->
RetryQueue.enqueue(params, Websub)
end
end
def perform(type, _) do
Logger.debug(fn -> "Unknown task: #{type}" end)
{:error, "Don't know what to do with this"}
end
def ap_enabled_actor(id) do
user = User.get_cached_by_ap_id(id)
if User.ap_enabled?(user) do
{:ok, user}
else
ActivityPub.make_user_from_ap_id(id)
end
end
end
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
new file mode 100644
index 000000000..916bcdcba
--- /dev/null
+++ b/lib/pleroma/web/federator/publisher.ex
@@ -0,0 +1,95 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Federator.Publisher do
+ alias Pleroma.Activity
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.Federator.RetryQueue
+
+ require Logger
+
+ @moduledoc """
+ Defines the contract used by federation implementations to publish messages to
+ their peers.
+ """
+
+ @doc """
+ Determine whether an activity can be relayed using the federation module.
+ """
+ @callback is_representable?(Pleroma.Activity.t()) :: boolean()
+
+ @doc """
+ Relays an activity to a specified peer, determined by the parameters. The
+ parameters used are controlled by the federation module.
+ """
+ @callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
+
+ @doc """
+ Enqueue publishing a single activity.
+ """
+ @spec enqueue_one(module(), Map.t()) :: :ok
+ def enqueue_one(module, %{} = params),
+ do: PleromaJobQueue.enqueue(:federation_outgoing, __MODULE__, [:publish_one, module, params])
+
+ @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
+ def perform(:publish_one, module, params) do
+ case apply(module, :publish_one, [params]) do
+ {:ok, _} ->
+ :ok
+
+ {:error, _e} ->
+ RetryQueue.enqueue(params, module)
+ end
+ end
+
+ def perform(type, _, _) do
+ Logger.debug("Unknown task: #{type}")
+ {:error, "Don't know what to do with this"}
+ end
+
+ @doc """
+ Relays an activity to all specified peers.
+ """
+ @callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()}
+
+ @spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok
+ def publish(%User{} = user, %Activity{} = activity) do
+ Config.get([:instance, :federation_publisher_modules])
+ |> Enum.each(fn module ->
+ if module.is_representable?(activity) do
+ Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}")
+ module.publish(user, activity)
+ end
+ end)
+
+ :ok
+ end
+
+ @doc """
+ Gathers links used by an outgoing federation module for WebFinger output.
+ """
+ @callback gather_webfinger_links(Pleroma.User.t()) :: list()
+
+ @spec gather_webfinger_links(Pleroma.User.t()) :: list()
+ def gather_webfinger_links(%User{} = user) do
+ Config.get([:instance, :federation_publisher_modules])
+ |> Enum.reduce([], fn module, links ->
+ links ++ module.gather_webfinger_links(user)
+ end)
+ end
+
+ @doc """
+ Gathers nodeinfo protocol names supported by the federation module.
+ """
+ @callback gather_nodeinfo_protocol_names() :: list()
+
+ @spec gather_nodeinfo_protocol_names() :: list()
+ def gather_nodeinfo_protocol_names do
+ Config.get([:instance, :federation_publisher_modules])
+ |> Enum.reduce([], fn module, links ->
+ links ++ module.gather_nodeinfo_protocol_names()
+ end)
+ end
+end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 216a962bd..3bf2a0fbc 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -1,217 +1,218 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
alias Pleroma.Config
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Web.Federator.Publisher
plug(Pleroma.Web.FederatingPlug)
def schemas(conn, _params) do
response = %{
links: [
%{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: Web.base_url() <> "/nodeinfo/2.0.json"
},
%{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
href: Web.base_url() <> "/nodeinfo/2.1.json"
}
]
}
json(conn, response)
end
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
# under software.
def raw_nodeinfo do
instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
chat = Application.get_env(:pleroma, :chat)
gopher = Application.get_env(:pleroma, :gopher)
stats = Stats.get_stats()
mrf_simple =
Application.get_env(:pleroma, :mrf_simple)
|> Enum.into(%{})
# This horror is needed to convert regex sigils to strings
mrf_keyword =
Application.get_env(:pleroma, :mrf_keyword, [])
|> Enum.map(fn {key, value} ->
{key,
Enum.map(value, fn
{pattern, replacement} ->
%{
"pattern" =>
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end,
"replacement" => replacement
}
pattern ->
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end
end)}
end)
|> Enum.into(%{})
mrf_policies =
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
quarantined = Keyword.get(instance, :quarantined_instances)
quarantined =
if is_list(quarantined) do
quarantined
else
[]
end
staff_accounts =
User.all_superusers()
|> Enum.map(fn u -> u.ap_id end)
mrf_user_allowlist =
Config.get([:mrf_user_allowlist], [])
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
federation_response =
if Keyword.get(instance, :mrf_transparency) do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
mrf_keyword: mrf_keyword,
mrf_user_allowlist: mrf_user_allowlist,
quarantined_instances: quarantined
}
else
%{}
end
features =
[
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
if Keyword.get(media_proxy, :enabled) do
"media_proxy"
end,
if Keyword.get(gopher, :enabled) do
"gopher"
end,
if Keyword.get(chat, :enabled) do
"chat"
end,
if Keyword.get(suggestions, :enabled) do
"suggestions"
end,
if Keyword.get(instance, :allow_relay) do
"relay"
end,
if Keyword.get(instance, :safe_dm_mentions) do
"safe_dm_mentions"
end
]
|> Enum.filter(& &1)
%{
version: "2.0",
software: %{
name: Pleroma.Application.name() |> String.downcase(),
version: Pleroma.Application.version()
},
- protocols: ["ostatus", "activitypub"],
+ protocols: Publisher.gather_nodeinfo_protocol_names(),
services: %{
inbound: [],
outbound: []
},
openRegistrations: Keyword.get(instance, :registrations_open),
usage: %{
users: %{
total: stats.user_count || 0
},
localPosts: stats.status_count || 0
},
metadata: %{
nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description),
private: !Keyword.get(instance, :public, true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""),
timeout: Keyword.get(suggestions, :timeout, 5000),
limit: Keyword.get(suggestions, :limit, 23),
web: Keyword.get(suggestions, :web, "")
},
staffAccounts: staff_accounts,
federation: federation_response,
postFormats: Keyword.get(instance, :allowed_post_formats),
uploadLimits: %{
general: Keyword.get(instance, :upload_limit),
avatar: Keyword.get(instance, :avatar_upload_limit),
banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit)
},
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
features: features,
restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
}
}
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
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)
end
def nodeinfo(conn, _) do
conn
|> put_status(404)
|> json(%{error: "Nodeinfo schema version not handled"})
end
end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 4744c6d83..61515b31e 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -1,403 +1,404 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus do
@httpoison Application.get_env(:pleroma, :httpoison)
import Ecto.Query
import Pleroma.Web.XML
require Logger
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler
alias Pleroma.Web.OStatus.UnfollowHandler
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
def is_representable?(%Activity{} = activity) do
object = Object.normalize(activity)
cond do
is_nil(object) ->
false
- object.data["type"] == "Note" ->
+ Visibility.is_public?(activity) && object.data["type"] == "Note" ->
true
true ->
false
end
end
def feed_path(user) do
"#{user.ap_id}/feed.atom"
end
def pubsub_path(user) do
"#{Web.base_url()}/push/hub/#{user.nickname}"
end
def salmon_path(user) do
"#{user.ap_id}/salmon"
end
def remote_follow_path do
"#{Web.base_url()}/ostatus_subscribe?acct={uri}"
end
def handle_incoming(xml_string) do
with doc when doc != :error <- parse_document(xml_string) do
with {:ok, actor_user} <- find_make_or_update_user(doc),
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
entries = :xmerl_xpath.string('//entry', doc)
activities =
Enum.map(entries, fn entry ->
{:xmlObj, :string, object_type} =
:xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
{:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
Logger.debug("Handling #{verb}")
try do
case verb do
'http://activitystrea.ms/schema/1.0/delete' ->
with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/follow' ->
with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/unfollow' ->
with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/share' ->
with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
do: [activity, retweeted_activity]
'http://activitystrea.ms/schema/1.0/favorite' ->
with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
do: [activity, favorited_activity]
_ ->
case object_type do
'http://activitystrea.ms/schema/1.0/note' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/comment' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
_ ->
Logger.error("Couldn't parse incoming document")
nil
end
end
rescue
e ->
Logger.error("Error occured while handling activity")
Logger.error(xml_string)
Logger.error(inspect(e))
nil
end
end)
|> Enum.filter(& &1)
{:ok, activities}
else
_e -> {:error, []}
end
end
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
end
end
def handle_share(entry, doc) do
with {:ok, retweeted_activity} <- get_or_build_object(entry),
{:ok, activity} <- make_share(entry, doc, retweeted_activity) do
{:ok, activity, retweeted_activity}
else
e -> {:error, e}
end
end
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}
end
end
def get_or_build_object(entry) do
with {:ok, activity} <- get_or_try_fetching(entry) do
{:ok, activity}
else
_e ->
with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
NoteHandler.handle_note(object, object)
end
end
end
def get_or_try_fetching(entry) do
Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
{:ok, activity}
else
_ ->
Logger.debug("Couldn't get, will try to fetch")
with href when not is_nil(href) <-
string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
{:ok, favorited_activity}
else
e -> Logger.debug("Couldn't find href: #{inspect(e)}")
end
end
end
def handle_favorite(entry, doc) do
with {:ok, favorited_activity} <- get_or_try_fetching(entry),
{:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
{:ok, activity, favorited_activity}
else
e -> {:error, e}
end
end
def get_attachments(entry) do
:xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
|> Enum.map(fn enclosure ->
with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
%{
"type" => "Attachment",
"url" => [
%{
"type" => "Link",
"mediaType" => type,
"href" => href
}
]
}
end
end)
|> Enum.filter(& &1)
end
@doc """
Gets the content from a an entry.
"""
def get_content(entry) do
string_from_xpath("//content", entry)
end
@doc """
Get the cw that mastodon uses.
"""
def get_cw(entry) do
with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw
else
_e -> nil
end
end
def get_tags(entry) do
:xmerl_xpath.string('//category', entry)
|> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
|> Enum.filter(& &1)
|> Enum.map(&String.downcase/1)
end
def maybe_update(doc, user) do
if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
else
maybe_update_ostatus(doc, user)
end
end
def maybe_update_ostatus(doc, user) do
old_data = %{
avatar: user.avatar,
bio: user.bio,
name: user.name
}
with false <- user.local,
avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc),
new_data <- %{
avatar: avatar || old_data.avatar,
name: name || old_data.name,
bio: bio || old_data.bio
},
false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data)
User.update_and_set_cache(change)
else
_ ->
{:ok, user}
end
end
def find_make_or_update_user(doc) do
uri = string_from_xpath("//author/uri[1]", doc)
with {:ok, user} <- find_or_make_user(uri) do
maybe_update(doc, user)
end
end
def find_or_make_user(uri) do
query = from(user in User, where: user.ap_id == ^uri)
user = Repo.one(query)
if is_nil(user) do
make_user(uri)
else
{:ok, user}
end
end
def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do
data = %{
name: info["name"],
nickname: info["nickname"] <> "@" <> info["host"],
ap_id: info["uri"],
info: info,
avatar: info["avatar"],
bio: info["bio"]
}
with false <- update,
%User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
{:ok, user}
else
_e -> User.insert_or_update_user(data)
end
end
end
# TODO: Just takes the first one for now.
def make_avatar_object(author_doc, rel \\ "avatar") do
href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
if href do
%{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => type,
"href" => href
}
]
}
else
nil
end
end
def gather_user_info(username) do
with {:ok, webfinger_data} <- WebFinger.finger(username),
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
{:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
else
e ->
Logger.debug(fn -> "Couldn't gather info for #{username}" end)
{:error, e}
end
end
# Regex-based 'parsing' so we don't have to pull in a full html parser
# It's a hack anyway. Maybe revisit this in the future
@mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
@gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
@gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
def get_atom_url(body) do
cond do
Regex.match?(@mastodon_regex, body) ->
[[_, match]] = Regex.scan(@mastodon_regex, body)
{:ok, match}
Regex.match?(@gs_regex, body) ->
[[_, match]] = Regex.scan(@gs_regex, body)
{:ok, match}
Regex.match?(@gs_classic_regex, body) ->
[[_, match]] = Regex.scan(@gs_classic_regex, body)
{:ok, match}
true ->
Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
{:error, "Couldn't find the Atom link"}
end
end
def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
url,
[{:Accept, "application/atom+xml"}]
) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
def fetch_activity_from_html_url(url) do
Logger.debug("Trying to fetch #{url}")
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body}} <- @httpoison.get(url, []),
{:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
def fetch_activity_from_url(url) do
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
{:ok, activities}
else
_e -> fetch_activity_from_html_url(url)
end
rescue
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
{:error, "Couldn't get #{url}: #{inspect(e)}"}
end
end
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 0a9e51656..42709ab47 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -1,243 +1,269 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon do
+ @behaviour Pleroma.Web.Federator.Publisher
+
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
+ alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.XML
require Logger
def decode(salmon) do
doc = XML.parse_document(salmon)
{:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
{:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
{:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
{:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
{:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
{:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
{:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
alg = to_string(alg)
encoding = to_string(encoding)
type = to_string(type)
[data, type, encoding, alg, sig]
end
def fetch_magic_key(salmon) do
with [data, _, _, _, _] <- decode(salmon),
doc <- XML.parse_document(data),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, public_key} <- User.get_public_key_for_ap_id(uri),
magic_key <- encode_key(public_key) do
{:ok, magic_key}
end
end
def decode_and_validate(magickey, salmon) do
[data, type, encoding, alg, sig] = decode(salmon)
signed_text =
[data, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
key = decode_key(magickey)
verify = :public_key.verify(signed_text, :sha256, sig, key)
if verify do
{:ok, data}
else
:error
end
end
def decode_key("RSA." <> magickey) do
make_integer = fn bin ->
list = :erlang.binary_to_list(bin)
Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
end
[modulus, exponent] =
magickey
|> String.split(".")
|> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
|> Enum.map(make_integer)
{:RSAPublicKey, modulus, exponent}
end
def encode_key({:RSAPublicKey, modulus, exponent}) do
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
"RSA.#{modulus_enc}.#{exponent_enc}"
end
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65_537})
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem}
end
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
end
def encode(private_key, doc) do
type = "application/atom+xml"
encoding = "base64url"
alg = "RSA-SHA256"
signed_text =
[doc, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signature =
signed_text
|> :public_key.sign(:sha256, private_key)
|> to_string
|> Base.url_encode64()
doc_base64 =
doc
|> Base.url_encode64()
# Don't need proper xml building, these strings are safe to leave unescaped
salmon = """
<?xml version="1.0" encoding="UTF-8"?>
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
<me:data type="application/atom+xml">#{doc_base64}</me:data>
<me:encoding>#{encoding}</me:encoding>
<me:alg>#{alg}</me:alg>
<me:sig>#{signature}</me:sig>
</me:env>
"""
{:ok, salmon}
end
def remote_users(%{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || [])
to
|> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn user -> user && !user.local end)
end
@doc "Pushes an activity to remote account."
- def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params),
- do: send_to_user(Map.put(params, :recipient, salmon))
+ def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
+ do: publish_one(Map.put(params, :recipient, salmon))
- def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do
+ def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
with {:ok, %{status: code}} when code in 200..299 <-
- poster.(
+ @httpoison.post(
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
:ok
else
e ->
unless params[:unreachable_since], do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
- :error
+ {:error, "Unreachable instance"}
end
end
- def send_to_user(_), do: :noop
+ def publish_one(_), do: :noop
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
+ def is_representable?(%Activity{data: %{"type" => type}} = activity)
+ when type in @supported_activities,
+ do: Visibility.is_public?(activity)
+
+ def is_representable?(_), do: false
+
@doc """
Publishes an activity to remote accounts
"""
- @spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none
- def publish(user, activity, poster \\ &@httpoison.post/3)
+ @spec publish(User.t(), Pleroma.Activity.t()) :: none
+ def publish(user, activity)
- def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster)
+ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true)
if feed do
feed =
ActivityRepresenter.wrap_with_entry(feed)
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
{:ok, private, _} = keys_from_pem(keys)
{:ok, feed} = encode(private, feed)
remote_users = remote_users(activity)
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
reachable_urls = Map.keys(reachable_urls_metadata)
remote_users
|> Enum.filter(&(&1.info.salmon in reachable_urls))
|> Enum.each(fn remote_user ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
- Pleroma.Web.Federator.publish_single_salmon(%{
+ Publisher.enqueue_one(__MODULE__, %{
recipient: remote_user,
feed: feed,
- poster: poster,
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
})
end)
end
end
- def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
+ def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
+
+ def gather_webfinger_links(%User{} = user) do
+ {:ok, _private, public} = keys_from_pem(user.info.keys)
+ magic_key = encode_key(public)
+
+ [
+ %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
+ %{
+ "rel" => "magic-public-key",
+ "href" => "data:application/magic-public-key,#{magic_key}"
+ }
+ ]
+ end
+
+ def gather_nodeinfo_protocol_names, do: []
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index a3b0bf999..3a3b98a10 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,287 +1,257 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
@httpoison Application.get_env(:pleroma, :httpoison)
alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.Web.OStatus
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
require Logger
def host_meta do
base_url = Web.base_url()
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
{
:Link,
%{
rel: "lrdd",
type: "application/xrd+xml",
template: "#{base_url}/.well-known/webfinger?resource={uri}"
}
}
}
|> XmlBuilder.to_doc()
end
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_cached_by_nickname(username) do
{:ok, represent_user(user, fmt)}
else
_e ->
with %User{} = user <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, fmt)}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
+ defp gather_links(%User{} = user) do
+ [
+ %{
+ "rel" => "http://webfinger.net/rel/profile-page",
+ "type" => "text/html",
+ "href" => user.ap_id
+ }
+ ] ++ Publisher.gather_webfinger_links(user)
+ end
+
def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user)
- {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
- magic_key = Salmon.encode_key(public)
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id],
- "links" => [
- %{
- "rel" => "http://schemas.google.com/g/2010#updates-from",
- "type" => "application/atom+xml",
- "href" => OStatus.feed_path(user)
- },
- %{
- "rel" => "http://webfinger.net/rel/profile-page",
- "type" => "text/html",
- "href" => user.ap_id
- },
- %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
- %{
- "rel" => "magic-public-key",
- "href" => "data:application/magic-public-key,#{magic_key}"
- },
- %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
- %{
- "rel" => "self",
- "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
- "href" => user.ap_id
- },
- %{
- "rel" => "http://ostatus.org/schema/1.0/subscribe",
- "template" => OStatus.remote_follow_path()
- }
- ]
+ "links" => gather_links(user)
}
end
def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user)
- {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
- magic_key = Salmon.encode_key(public)
+
+ links =
+ gather_links(user)
+ |> Enum.map(fn link -> {:Link, link} end)
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
- {:Alias, user.ap_id},
- {:Link,
- %{
- rel: "http://schemas.google.com/g/2010#updates-from",
- type: "application/atom+xml",
- href: OStatus.feed_path(user)
- }},
- {:Link,
- %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
- {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
- {:Link,
- %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
- {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
- {:Link,
- %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
- ]
+ {:Alias, user.ap_id}
+ ] ++ links
}
|> XmlBuilder.to_doc()
end
# This seems a better fit in Salmon
def ensure_keys_present(user) do
info = user.info
if info.keys do
{:ok, user}
else
{:ok, pem} = Salmon.generate_rsa_pem()
info_cng =
info
|> Pleroma.User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
User.update_and_set_cache(cng)
end
end
defp get_magic_key(magic_key) do
"data:application/magic-public-key," <> magic_key = magic_key
{:ok, magic_key}
rescue
MatchError -> {:error, "Missing magic key data."}
end
defp webfinger_from_xml(doc) do
with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
{:ok, magic_key} <- get_magic_key(magic_key),
topic <-
XML.string_from_xpath(
~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
doc
),
subject <- XML.string_from_xpath("//Subject", doc),
salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
subscribe_address <-
XML.string_from_xpath(
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
doc
),
ap_id <-
XML.string_from_xpath(
~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
doc
) do
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data}
else
{:error, e} ->
{:error, e}
e ->
{:error, e}
end
end
defp webfinger_from_json(doc) do
data =
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "ap_id", link["href"])
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
{_, "magic-public-key"} ->
"data:application/magic-public-key," <> magic_key = link["href"]
Map.put(data, "magic_key", magic_key)
{"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
Map.put(data, "topic", link["href"])
{_, "salmon"} ->
Map.put(data, "salmon", link["href"])
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
Map.put(data, "subscribe_address", link["template"])
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
end
end)
{:ok, data}
end
def get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd']/@template"
with doc when doc != :error <- XML.parse_document(body),
template when template != nil <- XML.string_from_xpath(xpath, doc) do
{:ok, template}
end
end
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
@httpoison.get("http://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
end
end
end
def finger(account) do
account = String.trim_leading(account, "@")
domain =
with [_name, domain] <- String.split(account, "@") do
domain
else
_e ->
URI.parse(account).host
end
address =
case find_lrdd_template(domain) do
{:ok, template} ->
String.replace(template, "{uri}", URI.encode(account))
_ ->
"https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
end
with response <-
@httpoison.get(
address,
Accept: "application/xrd+xml,application/jrd+json"
),
{:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
if doc != :error do
webfinger_from_xml(doc)
else
with {:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else
{:error, e} -> e
end
end
else
e ->
Logger.debug(fn -> "Couldn't finger #{account}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end
end
end
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 3ffa6b416..7ad0414ab 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -1,302 +1,333 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
+ alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.Websub.WebsubServerSubscription
alias Pleroma.Web.XML
require Logger
import Ecto.Query
+ @behaviour Pleroma.Web.Federator.Publisher
+
@httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do
challenge = Base.encode16(:crypto.strong_rand_bytes(8))
lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
lease_seconds = lease_seconds |> to_string
params = %{
"hub.challenge": challenge,
"hub.lease_seconds": lease_seconds,
"hub.topic": subscription.topic,
"hub.mode": "subscribe"
}
url = hd(String.split(subscription.callback, "?"))
query = URI.parse(subscription.callback).query || ""
params = Map.merge(params, URI.decode_query(query))
with {:ok, response} <- getter.(url, [], params: params),
^challenge <- response.body do
changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset)
else
e ->
Logger.debug("Couldn't verify subscription")
Logger.debug(inspect(e))
{:error, subscription}
end
end
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
+
+ def is_representable?(%Activity{data: %{"type" => type}} = activity)
+ when type in @supported_activities,
+ do: Visibility.is_public?(activity)
+
+ def is_representable?(_), do: false
+
def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
response =
user
|> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
query =
from(
sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until)
)
subscriptions = Repo.all(query)
callbacks = Enum.map(subscriptions, & &1.callback)
reachable_callbacks_metadata = Instances.filter_reachable(callbacks)
reachable_callbacks = Map.keys(reachable_callbacks_metadata)
subscriptions
|> Enum.filter(&(&1.callback in reachable_callbacks))
|> Enum.each(fn sub ->
data = %{
xml: response,
topic: topic,
callback: sub.callback,
secret: sub.secret,
unreachable_since: reachable_callbacks_metadata[sub.callback]
}
- Federator.publish_single_websub(data)
+ Publisher.enqueue_one(__MODULE__, data)
end)
end
def publish(_, _, _), do: ""
+ def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
+
def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end
def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
with {:ok, topic} <- valid_topic(params, user),
{:ok, lease_time} <- lease_time(params),
secret <- params["hub.secret"],
callback <- params["hub.callback"] do
subscription = get_subscription(topic, callback)
data = %{
state: subscription.state || "requested",
topic: topic,
secret: secret,
callback: callback
}
change = Changeset.change(subscription, data)
websub = Repo.insert_or_update!(change)
change =
Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
websub = Repo.update!(change)
Federator.verify_websub(websub)
{:ok, websub}
else
{:error, reason} ->
Logger.debug("Couldn't create subscription")
Logger.debug(inspect(reason))
{:error, reason}
end
end
def incoming_subscription_request(user, params) do
Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}")
{:error, "Invalid WebSub request"}
end
defp get_subscription(topic, callback) do
Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
%WebsubServerSubscription{}
end
# Temp hack for mastodon.
defp lease_time(%{"hub.lease_seconds" => ""}) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
{:ok, String.to_integer(lease_seconds)}
end
defp lease_time(_) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp valid_topic(%{"hub.topic" => topic}, user) do
if topic == OStatus.feed_path(user) do
{:ok, OStatus.feed_path(user)}
else
{:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
end
end
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
topic = subscribed.info.topic
# FIXME: Race condition, use transactions
{:ok, subscription} =
with subscription when not is_nil(subscription) <-
Repo.get_by(WebsubClientSubscription, topic: topic) do
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq()
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
Repo.update(change)
else
_e ->
subscription = %WebsubClientSubscription{
topic: topic,
hub: subscribed.info.hub,
subscribers: [subscriber.ap_id],
state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
user: subscribed
}
Repo.insert(subscription)
end
requester.(subscription)
end
def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
with {:ok, response} <- getter.(topic),
status when status in 200..299 <- response.status,
body <- response.body,
doc <- XML.parse_document(body),
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do
name = XML.string_from_xpath("/feed/author[1]/name", doc)
preferred_username = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
display_name = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
avatar = OStatus.make_avatar_object(doc)
bio = XML.string_from_xpath("/feed/author[1]/summary", doc)
{:ok,
%{
"uri" => uri,
"hub" => hub,
"nickname" => preferred_username || name,
"name" => display_name || name,
"host" => URI.parse(uri).host,
"avatar" => avatar,
"bio" => bio
}}
else
e ->
{:error, e}
end
end
def request_subscription(websub, poster \\ &@httpoison.post/3, timeout \\ 10_000) do
data = [
"hub.mode": "subscribe",
"hub.topic": websub.topic,
"hub.secret": websub.secret,
"hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id)
]
# This checks once a second if we are confirmed yet
websub_checker = fn ->
helper = fn helper ->
:timer.sleep(1000)
websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
if websub, do: websub, else: helper.(helper)
end
helper.(helper)
end
task = Task.async(websub_checker)
with {:ok, %{status: 202}} <-
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
{:ok, websub} <- Task.yield(task, timeout) do
{:ok, websub}
else
e ->
Task.shutdown(task)
change = Ecto.Changeset.change(websub, %{state: "rejected"})
{:ok, websub} = Repo.update(change)
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
Logger.debug(fn -> "error: #{inspect(e)}" end)
{:error, websub}
end
end
def refresh_subscriptions(delta \\ 60 * 60 * 24) do
Logger.debug("Refreshing subscriptions")
cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta)
query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
subs = Repo.all(query)
Enum.each(subs, fn sub ->
Federator.request_subscription(sub)
end)
end
def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status: code}} when code in 200..299 <-
@httpoison.post(
callback,
xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(callback)
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code}
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_reachable(callback)
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end)
{:error, response}
end
end
+
+ def gather_webfinger_links(%User{} = user) do
+ [
+ %{
+ "rel" => "http://schemas.google.com/g/2010#updates-from",
+ "type" => "application/atom+xml",
+ "href" => OStatus.feed_path(user)
+ },
+ %{
+ "rel" => "http://ostatus.org/schema/1.0/subscribe",
+ "template" => OStatus.remote_follow_path()
+ }
+ ]
+ end
+
+ def gather_nodeinfo_protocol_names, do: ["ostatus"]
end
diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex
index 88f8ce2a3..b58602c7b 100644
--- a/lib/xml_builder.ex
+++ b/lib/xml_builder.ex
@@ -1,44 +1,45 @@
defmodule Pleroma.XmlBuilder do
def to_xml({tag, attributes, content}) do
open_tag = make_open_tag(tag, attributes)
content_xml = to_xml(content)
"<#{open_tag}>#{content_xml}</#{tag}>"
end
def to_xml({tag, %{} = attributes}) do
open_tag = make_open_tag(tag, attributes)
"<#{open_tag} />"
end
def to_xml({tag, content}), do: to_xml({tag, %{}, content})
def to_xml(content) when is_binary(content) do
to_string(content)
end
def to_xml(content) when is_list(content) do
for element <- content do
to_xml(element)
end
|> Enum.join()
end
def to_xml(%NaiveDateTime{} = time) do
NaiveDateTime.to_iso8601(time)
end
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
defp make_open_tag(tag, attributes) do
attributes_string =
for {attribute, value} <- attributes do
+ value = String.replace(value, "\"", """)
"#{attribute}=\"#{value}\""
end
|> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 1e056b7ee..0f90aa1ac 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,1164 +1,1162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Tesla.Mock
import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
participations =
conversation.participations
|> Repo.preload(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
Enum.each(participations, fn participation ->
assert called(Pleroma.Web.Streamer.stream("participation", participation))
end)
end
end
end
describe "fetching restricted by visibility" do
test "it restricts by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
assert activities == [direct_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
assert activities == [unlisted_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
assert activities == [private_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
assert activities == [public_activity]
activities =
ActivityPub.fetch_activities([], %{
:visibility => ~w[private public],
"actor_id" => user.ap_id
})
assert activities == [public_activity, private_activity]
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
assert user.info.source_data
assert user.info.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
fetch_two =
ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
|> Base.encode64()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "doesn't drop activities with content being null" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity.id == new_activity.id
end
test "inserts a given map into the activity database, giving it an id if it has none." do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert is_binary(activity.data["id"])
given_id = "bla"
data = %{
"id" => given_id,
"actor" => user.ap_id,
"to" => [],
"context" => "blabla",
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert activity.data["id"] == given_id
assert activity.data["context"] == "blabla"
assert activity.data["context_id"]
end
test "adds a context when none is there" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Pleroma.Object.normalize(activity)
assert is_binary(activity.data["context"])
assert is_binary(object.data["context"])
assert activity.data["context_id"]
assert object.data["context_id"]
end
test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Object.normalize(activity.data["object"])
assert is_binary(object.data["id"])
assert %Object{} = Object.get_by_ap_id(activity.data["object"])
end
end
describe "create activities" do
test "removes doubled 'to' recipients" do
user = insert(:user)
{:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == user.ap_id
assert activity.recipients == ["user1", "user2", user.ap_id]
end
test "increases user note count only for public activities" do
user = insert(:user)
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "1",
"visibility" => "public"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "unlisted"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "private"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "3",
"visibility" => "direct"
})
user = User.get_cached_by_id(user.id)
assert user.info.note_count == 2
end
test "increases replies count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
ap_id = activity.data["id"]
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
# unlisted
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# private
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# direct
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
end
end
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
{:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
{:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
activities = ActivityPub.fetch_activities(["someone", "someone_else"])
assert length(activities) == 2
assert activities == [activity_one, activity_two]
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
end
end
test "doesn't return blocked activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
assert Enum.member?(activities, activity_one)
refute Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_four)
end
test "doesn't return announce activities concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
# Calling with 'with_muted' will deliver muted activities, too.
activities =
ActivityPub.fetch_activities([], %{
"muting_user" => user,
"with_muted" => true,
"skip_preload" => true
})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.follow(user, booster)
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
assert announce_activity.id == announce.id
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
{:ok, _unlisted_activity} =
CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
{:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
[activity] = ActivityPub.fetch_public_activities()
assert activity == listed_activity
end
test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1
assert Enum.at(activities, 0) == public
end
test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30)
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities()
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
later_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
last_expected = List.last(later_activities)
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
last = List.last(activities)
assert length(activities) == 10
assert last == last_expected
end
test "retrieves ids up to max_id" do
_first_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(20)
later_activities = ActivityBuilder.insert_list(10)
max_id = List.first(later_activities).id
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, user} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
end
describe "like an object" do
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
user = insert(:user)
user_two = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)
assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
[note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"])
assert note_activity.data["object"]["like_count"] == 1
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end
describe "unliking" do
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, _, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
end
end
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
user = insert(:user)
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
user = insert(:user)
# Unannouncing an object that is not announced does nothing
# {:ok, object} = ActivityPub.unannounce(user, object)
# assert object.data["announcement_count"] == 0
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
announce_activity.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
end
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} = ActivityPub.upload(file)
assert object.data["name"] == "an_image.jpg"
end
test "works with base64 encoded images" do
file = %{
"img" => data_uri()
}
{:ok, %Object{}} = ActivityPub.upload(file)
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = Repo.get_by(User, ap_id: activity.data["actor"])
followed = Repo.get_by(User, ap_id: activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "following / unfollowing" do
test "creates a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.follow(follower, followed)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
assert is_map(activity.data["object"])
assert activity.data["object"]["type"] == "Follow"
assert activity.data["object"]["object"] == followed.ap_id
assert activity.data["object"]["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
assert is_map(activity.data["object"])
assert activity.data["object"]["type"] == "Block"
assert activity.data["object"]["object"] == blocked.ap_id
assert activity.data["object"]["id"] == block_activity.data["id"]
end
end
describe "deletion" do
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.get_by_ap_id(note.data["object"]["id"])
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == note.data["object"]["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "decrements user note count only for public activities" do
user = insert(:user, info: %{note_count: 10})
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
assert user.info.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
{:ok, object} =
Object.get_by_ap_id(note.data["object"]["id"])
|> Object.change(%{
data: %{
"actor" => note.data["object"]["actor"],
"id" => note.data["object"]["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
end
describe "timeline post-processing" do
test "it filters broken threads" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, user1} = User.follow(user1, user3)
assert User.following?(user1, user3)
{:ok, user2} = User.follow(user2, user3)
assert User.following?(user2, user3)
{:ok, user3} = User.follow(user3, user2)
assert User.following?(user3, user2)
{:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
{:ok, private_activity_1} =
CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
{:ok, private_activity_2} =
CommonAPI.post(user2, %{
"status" => "hi 3",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_1.id
})
{:ok, private_activity_3} =
CommonAPI.post(user3, %{
"status" => "hi 4",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_2.id
})
activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
- assert [public_activity, private_activity_1, private_activity_3] ==
- activities
+ assert [public_activity, private_activity_1, private_activity_3] == activities
assert length(activities) == 3
activities = ActivityPub.contain_timeline(activities, user1)
assert [public_activity, private_activity_1] == activities
assert length(activities) == 2
end
end
describe "update" do
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address]
assert update.data["object"]["id"] == user_data["id"]
assert update.data["object"]["type"] == user_data["type"]
end
end
test "returned pinned statuses" do
Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_two.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_three.id, user)
user = refresh_record(user)
activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
assert 3 = length(activities)
end
test "it can create a Flag activity" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
context = Utils.generate_context_id()
content = "foobar"
reporter_ap_id = reporter.ap_id
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
assert {:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [activity],
content: content
})
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^activity_ap_id]
}
} = activity
end
describe "publish_one/1" do
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
- assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: nil
})
refute called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
- assert {:error, _} =
- ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
- assert {:error, _} =
- ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
- assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
refute called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
refute called(Instances.set_unreachable(inbox))
end
end
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
end
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 52729eb50..0f43bc8f2 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -1,212 +1,217 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatorTest do
alias Pleroma.Instances
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
use Pleroma.DataCase
import Pleroma.Factory
import Mock
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "Publish an activity" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI"})
relay_mock = {
Pleroma.Web.ActivityPub.Relay,
[],
[publish: fn _activity -> send(self(), :relay_publish) end]
}
%{activity: activity, relay_mock: relay_mock}
end
test "with relays active, it publishes to the relay", %{
activity: activity,
relay_mock: relay_mock
} do
with_mocks([relay_mock]) do
Federator.publish(activity)
end
assert_received :relay_publish
end
test "with relays deactivated, it does not publish to the relay", %{
activity: activity,
relay_mock: relay_mock
} do
Pleroma.Config.put([:instance, :allow_relay], false)
with_mocks([relay_mock]) do
Federator.publish(activity)
end
refute_received :relay_publish
Pleroma.Config.put([:instance, :allow_relay], true)
end
end
describe "Targets reachability filtering in `publish`" do
test_with_mock "it federates only to reachable instances via AP",
- Federator,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
user = insert(:user)
{inbox1, inbox2} =
{"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"}
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
info: %{ap_enabled: true, source_data: %{"inbox" => inbox1}}
})
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
info: %{ap_enabled: true, source_data: %{"inbox" => inbox2}}
})
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(inbox1, dt)
Instances.set_consistently_unreachable(URI.parse(inbox2).host)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
- assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt}))
+ assert called(
+ Pleroma.Web.ActivityPub.Publisher.publish_one(%{
+ inbox: inbox1,
+ unreachable_since: dt
+ })
+ )
- refute called(Federator.publish_single_ap(%{inbox: inbox2}))
+ refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2}))
end
test_with_mock "it federates only to reachable instances via Websub",
- Federator,
+ Pleroma.Web.Websub,
[:passthrough],
[] do
user = insert(:user)
websub_topic = Pleroma.Web.OStatus.feed_path(user)
sub1 =
insert(:websub_subscription, %{
topic: websub_topic,
state: "active",
callback: "http://pleroma.soykaf.com/cb"
})
sub2 =
insert(:websub_subscription, %{
topic: websub_topic,
state: "active",
callback: "https://pleroma2.soykaf.com/cb"
})
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(sub2.callback, dt)
Instances.set_consistently_unreachable(sub1.callback)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
assert called(
- Federator.publish_single_websub(%{
+ Pleroma.Web.Websub.publish_one(%{
callback: sub2.callback,
unreachable_since: dt
})
)
- refute called(Federator.publish_single_websub(%{callback: sub1.callback}))
+ refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback}))
end
test_with_mock "it federates only to reachable instances via Salmon",
- Federator,
+ Pleroma.Web.Salmon,
[:passthrough],
[] do
user = insert(:user)
remote_user1 =
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
info: %{salmon: "https://domain.com/salmon"}
})
remote_user2 =
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
info: %{salmon: "https://domain2.com/salmon"}
})
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(remote_user2.ap_id, dt)
Instances.set_consistently_unreachable("domain.com")
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
assert called(
- Federator.publish_single_salmon(%{
+ Pleroma.Web.Salmon.publish_one(%{
recipient: remote_user2,
unreachable_since: dt
})
)
- refute called(Federator.publish_single_websub(%{recipient: remote_user1}))
+ refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1}))
end
end
describe "Receive an activity" do
test "successfully processes incoming AP docs with correct origin" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "hi world!",
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin"
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
{:ok, _activity} = Federator.incoming_ap_doc(params)
end
test "rejects incoming AP docs with incorrect origin" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "https://niu.moe/users/rye",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "hi world!",
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin"
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
:error = Federator.incoming_ap_doc(params)
end
end
end
diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs
index 7532578ca..232082779 100644
--- a/test/web/salmon/salmon_test.exs
+++ b/test/web/salmon/salmon_test.exs
@@ -1,111 +1,114 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon.SalmonTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
+ import Mock
import Pleroma.Factory
@magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
@wrong_magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAA"
@magickey_friendica "RSA.AMwa8FUs2fWEjX0xN7yRQgegQffhBpuKNC6fa5VNSVorFjGZhRrlPMn7TQOeihlc9lBz2OsHlIedbYn2uJ7yCs0.AQAB"
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "decodes a salmon" do
{:ok, salmon} = File.read("test/fixtures/salmon.xml")
{:ok, doc} = Salmon.decode_and_validate(@magickey, salmon)
assert Regex.match?(~r/xml/, doc)
end
test "errors on wrong magic key" do
{:ok, salmon} = File.read("test/fixtures/salmon.xml")
assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error
end
test "generates an RSA private key pem" do
{:ok, key} = Salmon.generate_rsa_pem()
assert is_binary(key)
assert Regex.match?(~r/RSA/, key)
end
test "it encodes a magic key from a public key" do
key = Salmon.decode_key(@magickey)
magic_key = Salmon.encode_key(key)
assert @magickey == magic_key
end
test "it decodes a friendica public key" do
_key = Salmon.decode_key(@magickey_friendica)
end
test "returns a public and private key from a pem" do
pem = File.read!("test/fixtures/private_key.pem")
{:ok, private, public} = Salmon.keys_from_pem(pem)
assert elem(private, 0) == :RSAPrivateKey
assert elem(public, 0) == :RSAPublicKey
end
test "encodes an xml payload with a private key" do
doc = File.read!("test/fixtures/incoming_note_activity.xml")
pem = File.read!("test/fixtures/private_key.pem")
{:ok, private, public} = Salmon.keys_from_pem(pem)
# Let's try a roundtrip.
{:ok, salmon} = Salmon.encode(private, doc)
{:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon)
assert doc == decoded_doc
end
test "it gets a magic key" do
salmon = File.read!("test/fixtures/salmon2.xml")
{:ok, key} = Salmon.fetch_magic_key(salmon)
assert key ==
"RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
end
- test "it pushes an activity to remote accounts it's addressed to" do
+ test_with_mock "it pushes an activity to remote accounts it's addressed to",
+ Publisher,
+ [:passthrough],
+ [] do
user_data = %{
info: %{
salmon: "http://test-example.org/salmon"
},
local: false
}
mentioned_user = insert(:user, user_data)
note = insert(:note)
activity_data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => note.data["actor"],
"to" => note.data["to"] ++ [mentioned_user.ap_id],
"object" => note.data,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"]
}
{:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]})
user = User.get_cached_by_ap_id(activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
- poster = fn url, _data, _headers ->
- assert url == "http://test-example.org/salmon"
- end
+ Salmon.publish(user, activity)
- Salmon.publish(user, activity, poster)
+ assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user}))
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Aug 29, 12:03 PM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737065
Default Alt Text
(160 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment