Introduce relay

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-07-30 16:40:59 +02:00
parent 56467301a1
commit c51115bdbe
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
54 changed files with 3100 additions and 1038 deletions

View file

@ -16,6 +16,7 @@ config :mobilizon, :instance,
hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost", hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost",
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false, registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
repository: Mix.Project.config()[:source_url], repository: Mix.Project.config()[:source_url],
allow_relay: true,
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000, avatar_upload_limit: 2_000_000,
@ -109,6 +110,9 @@ config :auto_linker,
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
config :phoenix, :json_library, Jason config :phoenix, :json_library, Jason
config :http_signatures,
adapter: Mobilizon.Service.HTTPSignatures.Signature
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim, config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
endpoint: endpoint:
System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org", System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org",

View file

@ -11,7 +11,9 @@ config :mobilizon, MobilizonWeb.Endpoint,
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000 port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
], ],
url: [ url: [
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local" host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local",
port: 80,
scheme: "http"
], ],
debug_errors: true, debug_errors: true,
code_reloader: true, code_reloader: true,

View file

@ -56,6 +56,7 @@
"chai": "^4.2.0", "chai": "^4.2.0",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.7.0",
"eslint": "^6.0.1", "eslint": "^6.0.1",
"graphql-cli": "^3.0.12",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"patch-package": "^6.1.2", "patch-package": "^6.1.2",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",

View file

@ -98,6 +98,7 @@ export default class App extends Vue {
@import "~buefy/src/scss/components/tag"; @import "~buefy/src/scss/components/tag";
@import "~buefy/src/scss/components/taginput"; @import "~buefy/src/scss/components/taginput";
@import "~buefy/src/scss/components/upload"; @import "~buefy/src/scss/components/upload";
@import "~buefy/src/scss/components/radio";
.router-enter-active, .router-enter-active,
.router-leave-active { .router-leave-active {

View file

@ -145,7 +145,8 @@ export const CREATE_EVENT = gql`
$beginsOn: DateTime!, $beginsOn: DateTime!,
$picture: PictureInput, $picture: PictureInput,
$tags: [String], $tags: [String],
$physicalAddress: AddressInput! $physicalAddress: AddressInput,
$visibility: EventVisibility
) { ) {
createEvent( createEvent(
title: $title, title: $title,
@ -155,7 +156,8 @@ export const CREATE_EVENT = gql`
category: $category, category: $category,
picture: $picture, picture: $picture,
tags: $tags, tags: $tags,
physicalAddress: $physicalAddress physicalAddress: $physicalAddress,
visibility: $visibility
) { ) {
id, id,
uuid, uuid,

View file

@ -6,6 +6,11 @@
<div v-if="$apollo.loading">Loading...</div> <div v-if="$apollo.loading">Loading...</div>
<div class="columns is-centered" v-else> <div class="columns is-centered" v-else>
<form class="column is-two-thirds-desktop" @submit="createEvent"> <form class="column is-two-thirds-desktop" @submit="createEvent">
<h2 class="subtitle">
<translate>
General informations
</translate>
</h2>
<picture-upload v-model="pictureFile" /> <picture-upload v-model="pictureFile" />
<b-field :label="$gettext('Title')"> <b-field :label="$gettext('Title')">
@ -34,7 +39,28 @@
</b-select> </b-select>
</b-field> </b-field>
<button class="button is-primary"> <h2 class="subtitle">
<translate>
Visibility
</translate>
</h2>
<label class="label">{{ $gettext('Event visibility') }}</label>
<div class="field">
<b-radio v-model="event.visibility"
name="name"
:native-value="EventVisibility.PUBLIC">
<translate>Visible everywhere on the web (public)</translate>
</b-radio>
</div>
<div class="field">
<b-radio v-model="event.visibility"
name="name"
:native-value="EventVisibility.PRIVATE">
<translate>Only accessible through link and search (private)</translate>
</b-radio>
</div>
<button class="button is-primary">
<translate>Create my event</translate> <translate>Create my event</translate>
</button> </button>
</form> </form>
@ -60,6 +86,7 @@ import TagInput from '@/components/Event/TagInput.vue';
import { TAGS } from '@/graphql/tags'; import { TAGS } from '@/graphql/tags';
import { ITag } from '@/types/tag.model'; import { ITag } from '@/types/tag.model';
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue'; import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
import { EventVisibility } from '@/types/event.model';
@Component({ @Component({
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor }, components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
@ -79,6 +106,7 @@ export default class CreateEvent extends Vue {
categories: string[] = Object.keys(Category); categories: string[] = Object.keys(Category);
event: IEvent = new EventModel(); event: IEvent = new EventModel();
pictureFile: File | null = null; pictureFile: File | null = null;
EventVisibility = EventVisibility;
created() { created() {
const now = new Date(); const now = new Date();
@ -181,4 +209,4 @@ export default class CreateEvent extends Vue {
// } // }
// } // }
} }
</script> </script>

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,11 @@ defmodule Mix.Tasks.Mobilizon.Common do
end end
end end
def start_mobilizon do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
{:ok, _} = Application.ensure_all_started(:mobilizon)
end
def escape_sh_path(path) do def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end end

View file

@ -0,0 +1,65 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/mix/tasks/pleroma/relay.ex
defmodule Mix.Tasks.Mobilizon.Relay do
use Mix.Task
alias Mobilizon.Service.ActivityPub.Relay
alias Mix.Tasks.Mobilizon.Common
@shortdoc "Manages remote relays"
@moduledoc """
Manages remote relays
## Follow a remote relay
``mix mobilizon.relay follow <relay_url>``
Example: ``mix mobilizon.relay follow https://example.org/relay``
## Unfollow a remote relay
``mix mobilizon.relay unfollow <relay_url>``
Example: ``mix mobilizon.relay unfollow https://example.org/relay``
"""
def run(["follow", target]) do
Common.start_mobilizon()
case Relay.follow(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while following #{target}: #{inspect(e)}")
end
end
def run(["unfollow", target]) do
Common.start_mobilizon()
case Relay.unfollow(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while unfollowing #{target}: #{inspect(e)}")
end
end
def run(["accept", target]) do
Common.start_mobilizon()
case Relay.accept(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while accept #{target} follow: #{inspect(e)}")
end
end
end

View file

@ -163,7 +163,6 @@ defmodule Mobilizon.Actors.Actor do
]) ])
|> validate_required([ |> validate_required([
:url, :url,
:outbox_url,
:inbox_url, :inbox_url,
:type, :type,
:domain, :domain,
@ -184,6 +183,44 @@ defmodule Mobilizon.Actors.Actor do
changes changes
end end
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
vars = %{
"name" => Mobilizon.CommonConfig.get([:instance, :name], "Mobilizon"),
"summary" =>
Mobilizon.CommonConfig.get(
[:instance, :description],
"An internal service actor for this Mobilizon instance"
),
"url" => url,
"keys" => pem,
"preferred_username" => preferred_username,
"domain" => nil,
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"followers_url" => "#{url}/followers",
"following_url" => "#{url}/following",
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"type" => :Application
}
cast(%Actor{}, vars, [
:type,
:name,
:summary,
:url,
:keys,
:preferred_username,
:domain,
:inbox_url,
:followers_url,
:following_url,
:shared_inbox_url
])
end
@doc """ @doc """
Changeset for group creation Changeset for group creation
""" """
@ -240,6 +277,14 @@ defmodule Mobilizon.Actors.Actor do
:outbox_url, :outbox_url,
build_url(username, :outbox) build_url(username, :outbox)
) )
|> put_change(
:followers_url,
build_url(username, :followers)
)
|> put_change(
:following_url,
build_url(username, :following)
)
|> put_change( |> put_change(
:inbox_url, :inbox_url,
build_url(username, :inbox) build_url(username, :inbox)
@ -325,18 +370,30 @@ defmodule Mobilizon.Actors.Actor do
%{total: Task.await(total), elements: Task.await(elements)} %{total: Task.await(total), elements: Task.await(elements)}
end end
@spec get_full_followers(struct()) :: list() defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
def get_full_followers(%Actor{id: actor_id} = _actor) do from(
Repo.all( a in Actor,
from( join: f in Follower,
a in Actor, on: a.id == f.actor_id,
join: f in Follower, where: f.target_actor_id == ^actor_id
on: a.id == f.actor_id,
where: f.target_actor_id == ^actor_id
)
) )
end end
@spec get_full_followers(struct()) :: list()
def get_full_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> Repo.all()
end
@spec get_full_external_followers(struct()) :: list()
def get_full_external_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> where([a], not is_nil(a.domain))
|> Repo.all()
end
@doc """ @doc """
Get followings from an actor Get followings from an actor
@ -404,18 +461,19 @@ defmodule Mobilizon.Actors.Actor do
Make an actor follow another Make an actor follow another
""" """
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()} @spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
def follow(%Actor{} = followed, %Actor{} = follower, approved \\ true) do def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do
with {:suspended, false} <- {:suspended, followed.suspended}, with {:suspended, false} <- {:suspended, followed.suspended},
# Check if followed has blocked follower # Check if followed has blocked follower
{:already_following, false} <- {:already_following, following?(follower, followed)} do {:already_following, false} <- {:already_following, following?(follower, followed)} do
do_follow(follower, followed, approved) do_follow(follower, followed, approved, url)
else else
{:already_following, %Follower{}} -> {:already_following, %Follower{}} ->
{:error, {:error, :already_following,
"Could not follow actor: you are already following #{followed.preferred_username}"} "Could not follow actor: you are already following #{followed.preferred_username}"}
{:suspended, _} -> {:suspended, _} ->
{:error, "Could not follow actor: #{followed.preferred_username} has been suspended"} {:error, :suspended,
"Could not follow actor: #{followed.preferred_username} has been suspended"}
end end
end end
@ -433,13 +491,20 @@ defmodule Mobilizon.Actors.Actor do
end end
end end
@spec do_follow(struct(), struct(), boolean) :: @spec do_follow(struct(), struct(), boolean(), String.t()) ::
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()} {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved) do defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do
Logger.info(
"Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{
approved
})"
)
Actors.create_follower(%{ Actors.create_follower(%{
"actor_id" => follower.id, "actor_id" => follower.id,
"target_actor_id" => followed.id, "target_actor_id" => followed.id,
"approved" => approved "approved" => approved,
"url" => url
}) })
end end

View file

@ -297,7 +297,7 @@ defmodule Mobilizon.Actors do
{:ok, actor} {:ok, actor}
err -> err ->
Logger.error(inspect(err)) Logger.debug(inspect(err))
{:error, err} {:error, err}
end end
end end
@ -475,16 +475,16 @@ defmodule Mobilizon.Actors do
@spec get_or_fetch_by_url(String.t(), bool()) :: {:ok, Actor.t()} | {:error, String.t()} @spec get_or_fetch_by_url(String.t(), bool()) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do def get_or_fetch_by_url(url, preload \\ false) do
case get_actor_by_url(url, preload) do case get_actor_by_url(url, preload) do
{:ok, actor} -> {:ok, %Actor{} = actor} ->
{:ok, actor} {:ok, actor}
_ -> _ ->
case ActivityPub.make_actor_from_url(url, preload) do case ActivityPub.make_actor_from_url(url, preload) do
{:ok, actor} -> {:ok, %Actor{} = actor} ->
{:ok, actor} {:ok, actor}
_ -> _ ->
Logger.error("Could not fetch by AP id") Logger.warn("Could not fetch by AP id")
{:error, "Could not fetch by AP id"} {:error, "Could not fetch by AP id"}
end end
end end
@ -655,6 +655,18 @@ defmodule Mobilizon.Actors do
end end
end end
def get_or_create_service_actor_by_url(url, preferred_username \\ "relay") do
case get_actor_by_url(url) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
%{url: url, preferred_username: preferred_username}
|> Actor.relay_creation()
|> Repo.insert()
end
end
alias Mobilizon.Actors.Member alias Mobilizon.Actors.Member
@doc """ @doc """
@ -895,7 +907,7 @@ defmodule Mobilizon.Actors do
end end
@doc """ @doc """
Get a follower by the followed actor and following actor Get a follow by the followed actor and following actor
""" """
@spec get_follower(Actor.t(), Actor.t()) :: Follower.t() @spec get_follower(Actor.t(), Actor.t()) :: Follower.t()
def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do
@ -904,6 +916,19 @@ defmodule Mobilizon.Actors do
) )
end end
@doc """
Get a follow by the followed actor and following actor
"""
@spec get_follow_by_url(String.t()) :: Follower.t()
def get_follow_by_url(url) do
Repo.one(
from(f in Follower,
where: f.url == ^url,
preload: [:actor, :target_actor]
)
)
end
@doc """ @doc """
Creates a follower. Creates a follower.
@ -1009,7 +1034,7 @@ defmodule Mobilizon.Actors do
{:error, error} -> {:error, error} ->
Logger.error("Error while removing an upload file") Logger.error("Error while removing an upload file")
Logger.error(inspect(error)) Logger.debug(inspect(error))
{:ok, actor} {:ok, actor}
end end
end end

View file

@ -7,9 +7,11 @@ defmodule Mobilizon.Actors.Follower do
alias Mobilizon.Actors.Follower alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
@primary_key {:id, :binary_id, autogenerate: true}
schema "followers" do schema "followers" do
field(:approved, :boolean, default: false) field(:approved, :boolean, default: false)
field(:score, :integer, default: 1000) field(:url, :string)
belongs_to(:target_actor, Actor) belongs_to(:target_actor, Actor)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
end end
@ -17,8 +19,34 @@ defmodule Mobilizon.Actors.Follower do
@doc false @doc false
def changeset(%Follower{} = member, attrs) do def changeset(%Follower{} = member, attrs) do
member member
|> cast(attrs, [:score, :approved, :target_actor_id, :actor_id]) |> cast(attrs, [:url, :approved, :target_actor_id, :actor_id])
|> validate_required([:score, :approved, :target_actor_id, :actor_id]) |> generate_url()
|> validate_required([:url, :approved, :target_actor_id, :actor_id])
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index) |> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index)
end end
# If there's a blank URL that's because we're doing the first insert
defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do
case fetch_change(changeset, :url) do
{:ok, _url} -> changeset
:error -> do_generate_url(changeset)
end
end
# Most time just go with the given URL
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
defp do_generate_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
changeset
|> put_change(
:url,
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
)
|> put_change(
:id,
uuid
)
end
end end

View file

@ -32,7 +32,8 @@ defmodule Mobilizon.Events do
:tracks, :tracks,
:tags, :tags,
:participants, :participants,
:physical_address :physical_address,
:picture
] ]
) )
|> paginate(page, limit) |> paginate(page, limit)
@ -248,7 +249,8 @@ defmodule Mobilizon.Events do
:tracks, :tracks,
:tags, :tags,
:participants, :participants,
:physical_address :physical_address,
:picture
] ]
) )
) )

View file

@ -9,6 +9,8 @@ defmodule MobilizonWeb.API.Events do
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils alias MobilizonWeb.API.Utils
@visibility %{"PUBLIC" => :public, "PRIVATE" => :private}
@doc """ @doc """
Create an event Create an event
""" """

View file

@ -0,0 +1,51 @@
defmodule MobilizonWeb.API.Follows do
@moduledoc """
Common API for following, unfollowing, accepting and rejecting stuff.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.ActivityPub
require Logger
def follow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.follow(follower, followed) do
{:ok, activity, _} ->
{:ok, activity}
e ->
Logger.warn("Error while following actor: #{inspect(e)}")
{:error, e}
end
end
def unfollow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.unfollow(follower, followed) do
{:ok, activity, _} ->
{:ok, activity}
e ->
Logger.warn("Error while unfollowing actor: #{inspect(e)}")
{:error, e}
end
end
def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
Actor.following?(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
{:ok, activity, _} <-
ActivityPub.accept(
%{to: [follower.url], actor: followed.url, object: data},
activity_follow_url
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity}
else
%Follower{approved: true} ->
{:error, "Follow already accepted"}
end
end
end

View file

@ -9,7 +9,7 @@ defmodule MobilizonWeb.API.Utils do
Determines the full audience based on mentions for a public audience Determines the full audience based on mentions for a public audience
Audience is: Audience is:
* `to` : the mentionned actors, the eventual actor we're replying to and the public * `to` : the mentioned actors, the eventual actor we're replying to and the public
* `cc` : the actor's followers * `cc` : the actor's followers
""" """
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()} @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
@ -72,7 +72,9 @@ defmodule MobilizonWeb.API.Utils do
end end
end end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} def get_to_and_cc(_actor, mentions, _inReplyTo, {:list, _}) do
{mentions, []}
end
# def get_addressed_users(_, to) when is_list(to) do # def get_addressed_users(_, to) when is_list(to) do
# Actors.get(to) # Actors.get(to)
@ -138,7 +140,7 @@ defmodule MobilizonWeb.API.Utils do
make_content_html( make_content_html(
content, content,
tags, tags,
"text/plain" "text/html"
), ),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url), mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
addressed_users <- get_addressed_users(mentioned_users, nil), addressed_users <- get_addressed_users(mentioned_users, nil),

View file

@ -14,6 +14,19 @@ defmodule MobilizonWeb.ActivityPubController do
action_fallback(:errors) action_fallback(:errors)
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
conn
else
conn
|> put_status(404)
|> json("Not found")
|> halt()
end
end
def following(conn, %{"name" => name, "page" => page}) do def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page), with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
@ -67,6 +80,7 @@ defmodule MobilizonWeb.ActivityPubController do
# TODO: Ensure that this inbox is a recipient of the message # TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Logger.debug("Got something with valid signature inside inbox")
Federator.enqueue(:incoming_ap_doc, params) Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok") json(conn, "ok")
end end
@ -90,19 +104,35 @@ defmodule MobilizonWeb.ActivityPubController do
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!" "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
) )
Logger.error(inspect(conn.req_headers)) Logger.debug(inspect(conn.req_headers))
end end
json(conn, "error") json(conn, "error")
end end
def relay(conn, _params) do
with {status, actor} <-
Cachex.fetch(
:activity_pub,
"relay_actor",
&Mobilizon.Service.ActivityPub.Relay.get_actor/0
),
true <- status in [:ok, :commit] do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("actor.json", %{actor: actor}))
end
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(404) |> put_status(404)
|> json("Not found") |> json("Not found")
end end
def errors(conn, _e) do def errors(conn, e) do
Logger.debug(inspect(e))
conn conn
|> put_status(500) |> put_status(500)
|> json("Unknown Error") |> json("Unknown Error")

View file

@ -10,7 +10,6 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
Plug to check HTTP Signatures on every incoming request Plug to check HTTP Signatures on every incoming request
""" """
alias Mobilizon.Service.HTTPSignatures
import Plug.Conn import Plug.Conn
require Logger require Logger
@ -23,32 +22,30 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
end end
def call(conn, _opts) do def call(conn, _opts) do
actor = conn.params["actor"]
Logger.debug(fn ->
"Checking sig for #{actor}"
end)
[signature | _] = get_req_header(conn, "signature") [signature | _] = get_req_header(conn, "signature")
cond do if signature do
String.contains?(signature, actor) -> # set (request-target) header to the appropriate value
conn = # we also replace the digest header with the one we computed
conn conn =
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
signature ->
Logger.debug("Signature not from actor")
assign(conn, :valid_signature, false)
true ->
Logger.debug("No signature header!")
conn conn
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
conn =
if conn.assigns[:digest] do
conn
|> put_req_header("digest", conn.assigns[:digest])
else
conn
end
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
else
Logger.debug("No signature header!")
conn
end end
end end
end end

View file

@ -18,6 +18,10 @@ defmodule MobilizonWeb.Router do
plug(MobilizonWeb.HTTPSignaturePlug) plug(MobilizonWeb.HTTPSignaturePlug)
end end
pipeline :relay do
plug(:accepts, ["activity-json", "json"])
end
pipeline :activity_pub do pipeline :activity_pub do
plug(:accepts, ["activity-json"]) plug(:accepts, ["activity-json"])
end end
@ -97,6 +101,13 @@ defmodule MobilizonWeb.Router do
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
end end
scope "/relay", MobilizonWeb do
pipe_through(:relay)
get("/", ActivityPubController, :relay)
post("/inbox", ActivityPubController, :inbox)
end
scope "/proxy/", MobilizonWeb do scope "/proxy/", MobilizonWeb do
pipe_through(:remote_media) pipe_through(:remote_media)

View file

@ -12,12 +12,12 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
public_key = Mobilizon.Service.ActivityPub.Utils.pem_to_public_key_pem(actor.keys) public_key = Mobilizon.Service.ActivityPub.Utils.pem_to_public_key_pem(actor.keys)
%{ %{
"id" => Actor.build_url(actor.preferred_username, :page), "id" => actor.url,
"type" => "Person", "type" => to_string(actor.type),
"following" => Actor.build_url(actor.preferred_username, :following), "following" => actor.following_url,
"followers" => Actor.build_url(actor.preferred_username, :followers), "followers" => actor.followers_url,
"inbox" => Actor.build_url(actor.preferred_username, :inbox), "inbox" => actor.inbox_url,
"outbox" => Actor.build_url(actor.preferred_username, :outbox), "outbox" => actor.outbox_url,
"preferredUsername" => actor.preferred_username, "preferredUsername" => actor.preferred_username,
"name" => actor.name, "name" => actor.name,
"summary" => actor.summary, "summary" => actor.summary,

View file

@ -31,8 +31,8 @@ defmodule MobilizonWeb.ErrorView do
# template is found, let's render it as 500 # template is found, let's render it as 500
def template_not_found(template, assigns) do def template_not_found(template, assigns) do
require Logger require Logger
Logger.error("Template not found") Logger.warn("Template not found")
Logger.error(inspect(template)) Logger.debug(inspect(template))
render("500.html", assigns) render("500.html", assigns)
end end
end end

View file

@ -46,24 +46,8 @@ defmodule MobilizonWeb.PageView do
end end
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
event = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event) event
{:ok, html, []} = Earmark.as_html(event["summary"]) |> Mobilizon.Service.ActivityPub.Converters.Event.model_to_as()
%{
"type" => "Event",
"attributedTo" => event["actor"],
"id" => event["id"],
"name" => event["title"],
"category" => event["category"],
"content" => html,
"source" => %{
"content" => event["summary"],
"mediaType" => "text/markdown"
},
"mediaType" => "text/html",
"published" => event["publish_at"],
"updated" => event["updated_at"]
}
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end

View file

@ -21,10 +21,11 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Actors.Follower alias Mobilizon.Actors.Follower
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures alias Mobilizon.Service.HTTPSignatures.Signature
require Logger require Logger
import Mobilizon.Service.ActivityPub.Utils import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
@doc """ @doc """
Get recipients for an activity or object Get recipients for an activity or object
@ -42,10 +43,6 @@ defmodule Mobilizon.Service.ActivityPub do
def insert(map, local \\ true) when is_map(map) do def insert(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map), with map <- lazy_put_activity_defaults(map),
{:ok, object} <- insert_full_object(map) do {:ok, object} <- insert_full_object(map) do
object_id = if is_map(map["object"]), do: map["object"]["id"], else: map["id"]
map = if local, do: Map.put(map, "id", "#{object_id}/activity"), else: map
activity = %Activity{ activity = %Activity{
data: map, data: map,
local: local, local: local,
@ -94,7 +91,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do {:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
case data["type"] do case data["type"] do
"Event" -> "Event" ->
{:ok, Events.get_event_by_url!(object_url)} {:ok, Events.get_event_full_by_url!(object_url)}
"Note" -> "Note" ->
{:ok, Events.get_comment_full_from_url!(object_url)} {:ok, Events.get_comment_full_from_url!(object_url)}
@ -107,15 +104,17 @@ defmodule Mobilizon.Service.ActivityPub do
end end
else else
{:existing_event, %Event{url: event_url}} -> {:existing_event, %Event{url: event_url}} ->
{:ok, Events.get_event_by_url!(event_url)} {:ok, Events.get_event_full_by_url!(event_url)}
{:existing_comment, %Comment{url: comment_url}} -> {:existing_comment, %Comment{url: comment_url}} ->
{:ok, Events.get_comment_full_from_url!(comment_url)} {:ok, Events.get_comment_full_from_url!(comment_url)}
{:existing_actor, %Actor{url: actor_url}} -> {:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)} {:ok, Actors.get_actor_by_url!(actor_url, true)}
e -> e ->
require Logger
Logger.error(inspect(e))
{:error, e} {:error, e}
end end
end end
@ -137,24 +136,40 @@ defmodule Mobilizon.Service.ActivityPub do
%{to: to, actor: actor, published: published, object: object}, %{to: to, actor: actor, published: published, object: object},
additional additional
), ),
:ok <- Logger.debug(inspect(create_data)),
{:ok, activity, object} <- insert(create_data, local), {:ok, activity, object} <- insert(create_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do # {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object} {:ok, activity, object}
else else
err -> err ->
Logger.error("Something went wrong") Logger.error("Something went wrong while creating an activity")
Logger.error(inspect(err)) Logger.debug(inspect(err))
err err
end end
end end
def accept(%{to: to, actor: actor, object: object} = params) do def accept(%{to: to, actor: actor, object: object} = params, activity_follow_id \\ nil) do
# only accept false as false value # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object}, with data <- %{
"to" => to,
"type" => "Accept",
"actor" => actor,
"object" => object,
"id" => activity_follow_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
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.url, "object" => object},
{:ok, activity, object} <- insert(data, local), {:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
@ -168,6 +183,7 @@ defmodule Mobilizon.Service.ActivityPub do
with data <- %{ with data <- %{
"to" => to, "to" => to,
"cc" => cc, "cc" => cc,
"id" => object["url"],
"type" => "Update", "type" => "Update",
"actor" => actor, "actor" => actor,
"object" => object "object" => object
@ -215,55 +231,56 @@ defmodule Mobilizon.Service.ActivityPub do
# end # end
# end # end
# def announce( def announce(
# %Actor{} = actor, %Actor{} = actor,
# object, object,
# activity_id \\ nil, activity_id \\ nil,
# local \\ true local \\ true,
# ) do public \\ true
# #with true <- is_public?(object), ) do
# with announce_data <- make_announce_data(actor, object, activity_id), with true <- is_public?(object),
# {:ok, activity, object} <- insert(announce_data, local), announce_data <- make_announce_data(actor, object, activity_id, public),
# # {:ok, object} <- add_announce_to_object(activity, object), {:ok, activity, object} <- insert(announce_data, local),
# :ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
# {:ok, activity, object} {:ok, activity, object}
# else else
# error -> {:error, error} error ->
# end {:error, error}
# end end
end
# def unannounce( def unannounce(
# %Actor{} = actor, %Actor{} = actor,
# object, object,
# activity_id \\ nil, activity_id \\ nil,
# local \\ true cancelled_activity_id \\ nil,
# ) do local \\ true
# with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object), ) do
# unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
# {:ok, unannounce_activity, _object} <- insert(unannounce_data, local), unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
# :ok <- maybe_federate(unannounce_activity), {:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
# {:ok, _activity} <- Repo.delete(announce_activity), :ok <- maybe_federate(unannounce_activity) do
# {:ok, object} <- remove_announce_from_object(announce_activity, object) do {:ok, unannounce_activity, object}
# {:ok, unannounce_activity, object} else
# else _e -> {:ok, object}
# _e -> {:ok, object} end
# end end
# end
@doc """ @doc """
Make an actor follow another Make an actor follow another
""" """
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.follow(followed, follower, true), with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false),
activity_follow_id <- activity_follow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity", activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id), data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity, object} <- insert(data, local), {:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
{err, _} when err in [:already_following, :suspended] -> {:error, err, msg} when err in [:already_following, :suspended] ->
{:error, err} {:error, msg}
end end
end end
@ -271,18 +288,26 @@ defmodule Mobilizon.Service.ActivityPub do
Make an actor unfollow another Make an actor unfollow another
""" """
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = followed, %Actor{} = follower, activity_id \\ nil, local \\ true) do def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower), with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
# We recreate the follow activity # We recreate the follow activity
data <- make_follow_data(followed, follower, follow_id), data <-
make_follow_data(
followed,
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity, _object} <- insert(data, local), {:ok, follow_activity, _object} <- insert(data, local),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
{:ok, activity, object} <- insert(unfollow_data, local), {:ok, activity, object} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
err -> err ->
Logger.error(inspect(err)) Logger.debug("Error while unfollowing an actor #{inspect(err)}")
err err
end end
end end
@ -294,7 +319,8 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete", "type" => "Delete",
"actor" => actor.url, "actor" => actor.url,
"object" => url, "object" => url,
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
"id" => url <> "/delete"
} }
with {:ok, _} <- Events.delete_event(event), with {:ok, _} <- Events.delete_event(event),
@ -309,6 +335,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete", "type" => "Delete",
"actor" => actor.url, "actor" => actor.url,
"object" => url, "object" => url,
"id" => url <> "/delete",
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
} }
@ -324,6 +351,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete", "type" => "Delete",
"actor" => url, "actor" => url,
"object" => url, "object" => url,
"id" => url <> "/delete",
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] "to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
} }
@ -366,11 +394,11 @@ defmodule Mobilizon.Service.ActivityPub do
# Request returned 410 # Request returned 410
{:error, :actor_deleted} -> {:error, :actor_deleted} ->
Logger.info("Actor was deleted")
{:error, :actor_deleted} {:error, :actor_deleted}
e -> e ->
Logger.error("Failed to make actor from url") Logger.warn("Failed to make actor from url")
Logger.error(inspect(e))
{:error, e} {:error, e}
end end
end end
@ -414,10 +442,18 @@ defmodule Mobilizon.Service.ActivityPub do
""" """
def publish(actor, activity) do def publish(actor, activity) do
Logger.debug("Publishing an activity") Logger.debug("Publishing an activity")
Logger.debug(inspect(activity))
public = is_public?(activity)
if public && Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Mobilizon.Service.ActivityPub.Relay.publish(activity)
end
followers = followers =
if actor.followers_url in activity.recipients do if actor.followers_url in activity.recipients do
Actor.get_full_followers(actor) |> Enum.filter(fn follower -> is_nil(follower.domain) end) Actor.get_full_external_followers(actor)
else else
[] []
end end
@ -448,15 +484,16 @@ defmodule Mobilizon.Service.ActivityPub do
Logger.info("Federating #{id} to #{inbox}") Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox) %URI{host: host, path: path} = URI.parse(inbox)
digest = HTTPSignatures.build_digest(json) digest = Signature.build_digest(json)
date = HTTPSignatures.generate_date_header() date = Signature.generate_date_header()
request_target = HTTPSignatures.generate_request_target("POST", path) # request_target = Signature.generate_request_target("POST", path)
signature = signature =
HTTPSignatures.sign(actor, %{ Signature.sign(actor, %{
host: host, host: host,
"content-length": byte_size(json), "content-length": byte_size(json),
"(request-target)": request_target, # TODO : Look me up in depth why Pleroma handles this inside lib/mobilizon_web/http_signature.ex
# "(request-target)": request_target,
digest: digest, digest: digest,
date: date date: date
}) })
@ -478,20 +515,27 @@ defmodule Mobilizon.Service.ActivityPub do
@spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, struct()} | {:error, atom()} | any() @spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, struct()} | {:error, atom()} | any()
defp fetch_and_prepare_actor_from_url(url) do defp fetch_and_prepare_actor_from_url(url) do
Logger.debug("Fetching and preparing actor from url") Logger.debug("Fetching and preparing actor from url")
Logger.debug(inspect(url))
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- res =
HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true), with %HTTPoison.Response{status_code: 200, body: body} <-
{:ok, data} <- Jason.decode(body) do HTTPoison.get!(url, [Accept: "application/activity+json"], follow_redirect: true),
actor_data_from_actor_object(data) :ok <- Logger.debug("response okay, now decoding json"),
else {:ok, data} <- Jason.decode(body) do
# Actor is gone, probably deleted Logger.debug("Got activity+json response at actor's endpoint, now converting data")
{:ok, %HTTPoison.Response{status_code: 410}} -> actor_data_from_actor_object(data)
{:error, :actor_deleted} else
# Actor is gone, probably deleted
{:ok, %HTTPoison.Response{status_code: 410}} ->
Logger.info("Response HTTP 410")
{:error, :actor_deleted}
e -> e ->
Logger.error("Could not decode actor at fetch #{url}, #{inspect(e)}") Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
e {:error, e}
end end
res
end end
@doc """ @doc """

View file

@ -7,3 +7,10 @@ defmodule Mobilizon.Service.ActivityPub.Converter do
@callback as_to_model_data(map()) :: map() @callback as_to_model_data(map()) :: map()
@callback model_to_as(struct()) :: map() @callback model_to_as(struct()) :: map()
end end
defprotocol Mobilizon.Service.ActivityPub.Convertible do
@type activitystreams :: map()
@spec model_to_as(t) :: activitystreams
def model_to_as(convertible)
end

View file

@ -45,3 +45,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
} }
end end
end end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Actors.Actor do
alias Mobilizon.Service.ActivityPub.Converters.Actor, as: ActorConverter
defdelegate model_to_as(actor), to: ActorConverter
end

View file

@ -52,7 +52,29 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Address do
""" """
@impl Converter @impl Converter
@spec model_to_as(AddressModel.t()) :: map() @spec model_to_as(AddressModel.t()) :: map()
def model_to_as(%AddressModel{} = _address) do def model_to_as(%AddressModel{} = address) do
nil res = %{
"type" => "Place",
"name" => address.description,
"id" => address.url,
"address" => %{
"type" => "PostalAddress",
"streetAddress" => address.street,
"postalCode" => address.postal_code,
"addressLocality" => address.locality,
"addressRegion" => address.region,
"addressCountry" => address.country
}
}
if is_nil(address.geom) do
res
else
Map.put(res, "geo", %{
"type" => "GeoCoordinates",
"latitude" => address.geom.coordinates |> elem(0),
"longitude" => address.geom.coordinates |> elem(1)
})
end
end end
end end

View file

@ -84,7 +84,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
"actor" => comment.actor.url, "actor" => comment.actor.url,
"attributedTo" => comment.actor.url, "attributedTo" => comment.actor.url,
"uuid" => comment.uuid, "uuid" => comment.uuid,
"id" => Routes.page_url(Endpoint, :comment, comment.uuid) "id" => comment.url
} }
if comment.in_reply_to_comment do if comment.in_reply_to_comment do
@ -94,3 +94,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
end end
end end
end end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Events.Comment do
alias Mobilizon.Service.ActivityPub.Converters.Comment, as: CommentConverter
defdelegate model_to_as(comment), to: CommentConverter
end

View file

@ -10,6 +10,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Service.ActivityPub.Converter alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub.Converters.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.Tag alias Mobilizon.Events.Tag
alias Mobilizon.Addresses alias Mobilizon.Addresses
@ -26,6 +28,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
@spec as_to_model_data(map()) :: map() @spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do def as_to_model_data(object) do
Logger.debug("event as_to_model_data") Logger.debug("event as_to_model_data")
Logger.debug(inspect(object))
with {:actor, {:ok, %Actor{id: actor_id}}} <- with {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, Actors.get_actor_by_url(object["actor"])}, {:actor, Actors.get_actor_by_url(object["actor"])},
@ -99,6 +102,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
end end
defp fetch_tags(tags) do defp fetch_tags(tags) do
Logger.debug("fetching tags")
Enum.reduce(tags, [], fn tag, acc -> Enum.reduce(tags, [], fn tag, acc ->
with true <- tag["type"] == "Hashtag", with true <- tag["type"] == "Hashtag",
{:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do {:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do
@ -110,23 +115,62 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
end) end)
end end
defp build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
"name" => "##{tag.title}",
"type" => "Hashtag"
}
end)
end
@doc """ @doc """
Convert an event struct to an ActivityStream representation Convert an event struct to an ActivityStream representation
""" """
@impl Converter @impl Converter
@spec model_to_as(EventModel.t()) :: map() @spec model_to_as(EventModel.t()) :: map()
def model_to_as(%EventModel{} = event) do def model_to_as(%EventModel{} = event) do
%{ to =
if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [event.organizer_actor.followers_url]
res = %{
"type" => "Event", "type" => "Event",
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => to,
"title" => event.title, "cc" => [],
"attributedTo" => event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url, "actor" => event.organizer_actor.url,
"uuid" => event.uuid, "uuid" => event.uuid,
"category" => event.category, "category" => event.category,
"summary" => event.description, "content" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(), "publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(), "updated_at" => event.updated_at |> date_to_string(),
"mediaType" => "text/html",
"startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"id" => event.url "id" => event.url
} }
res =
if is_nil(event.physical_address),
do: res,
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address))
if is_nil(event.picture),
do: res,
else: Map.put(res, "attachment", [Utils.make_picture_data(event.picture)])
end end
defp date_to_string(nil), do: nil
defp date_to_string(date), do: DateTime.to_iso8601(date)
end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Events.Event do
alias Mobilizon.Service.ActivityPub.Converters.Event, as: EventConverter
defdelegate model_to_as(event), to: EventConverter
end end

View file

@ -0,0 +1,88 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/relay.ex
defmodule Mobilizon.Service.ActivityPub.Relay do
@moduledoc """
Handles following and unfollowing relays and instances
"""
alias Mobilizon.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.API.Follows
require Logger
def get_actor do
with {:ok, %Actor{} = actor} <-
Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
actor
end
end
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
e ->
Logger.warn("Error while following remote instance: #{inspect(e)}")
{:error, e}
end
end
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
e ->
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
{:error, e}
end
end
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
end
end
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end
# end
@doc """
Publish an activity to all relays following this instance
"""
def publish(%Activity{data: %{"object" => object}} = _activity) do
with %Actor{id: actor_id} = actor <- get_actor(),
{:ok, object} <-
Mobilizon.Service.ActivityPub.Transmogrifier.fetch_obj_helper_as_activity_streams(
object
) do
ActivityPub.announce(actor, object, "#{object["id"]}/announces/#{actor_id}", true, false)
else
e ->
Logger.error("Error while getting local instance actor: #{inspect(e)}")
end
end
def publish(err) do
Logger.error("Tried to publish a bad activity")
Logger.debug(inspect(err))
nil
end
end

View file

@ -8,11 +8,12 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back. A module to handle coding from internal to wire ActivityPub and back.
""" """
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment} alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Visibility
require Logger require Logger
@ -45,7 +46,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("actor", object["attributedTo"]) |> Map.put("actor", object["attributedTo"])
|> fix_attachments |> fix_attachments
|> fix_in_reply_to |> fix_in_reply_to
|> fix_tag
# |> fix_tag
end end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
@ -69,8 +71,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) do when not is_nil(in_reply_to) do
Logger.error("inReplyTo ID seem incorrect") Logger.warn("inReplyTo ID seem incorrect: #{inspect(in_reply_to)}")
Logger.error(inspect(in_reply_to))
do_fix_in_reply_to("", object) do_fix_in_reply_to("", object)
end end
@ -87,7 +88,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
object object
e -> e ->
Logger.error("Couldn't fetch #{in_reply_to_id} #{inspect(e)}") Logger.warn("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
object object
end end
end end
@ -116,6 +117,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("tag", combined) |> Map.put("tag", combined)
end end
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
def handle_incoming(%{"type" => "Flag"} = data) do def handle_incoming(%{"type" => "Flag"} = data) do
with params <- Mobilizon.Service.ActivityPub.Converters.Flag.as_to_model(data) do with params <- Mobilizon.Service.ActivityPub.Converters.Flag.as_to_model(data) do
params = %{ params = %{
@ -186,13 +190,69 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true), with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower), {:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
{:ok, activity, object} {:ok, activity, object}
else else
e -> e ->
Logger.error("Unable to handle Follow activity") Logger.warn("Unable to handle Follow activity #{inspect(e)}")
Logger.error(inspect(e)) :error
end
end
# TODO : Handle object being a Link
def handle_incoming(
%{
"type" => "Accept",
"object" => follow_object,
"actor" => _actor,
"id" => _id
} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, _} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: followed.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity, follow}
else
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
e ->
Logger.warn("Unable to process Accept Follow activity #{inspect(e)}")
:error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, object} <-
ActivityPub.reject(%{
to: [follower.url],
type: "Reject",
actor: followed,
object: follow_object,
local: false
}),
{:ok, _follower} <- Actor.unfollow(followed, follower) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error :error
end end
end end
@ -211,19 +271,21 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end # end
# end # end
# # # #
# def handle_incoming( def handle_incoming(
# %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
# ) do ) do
# with actor <- get_actor(data), with actor <- get_actor(data),
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
# {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do public <- Visibility.is_public?(data),
# {:ok, activity} {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
# else {:ok, activity, object}
# e -> Logger.error(inspect e) else
# :error e ->
# end Logger.debug(inspect(e))
# end :error
end
end
def handle_incoming( def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} = %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
@ -245,28 +307,33 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
}) })
e -> e ->
Logger.error(inspect(e)) Logger.debug(inspect(e))
:error :error
end end
end end
# def handle_incoming( def handle_incoming(
# %{ %{
# "type" => "Undo", "type" => "Undo",
# "object" => %{"type" => "Announce", "object" => object_id}, "object" => %{
# "actor" => actor, "type" => "Announce",
# "id" => id "object" => object_id,
# } = data "id" => cancelled_activity_id
# ) do },
# with actor <- get_actor(data), "actor" => actor,
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), "id" => id
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), } = data
# {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do ) do
# {:ok, activity} with actor <- get_actor(data),
# else {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
# _e -> :error {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
# end {:ok, activity, object} <-
# end ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
{:ok, activity, object}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
@ -278,12 +345,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
) do ) do
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed), with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower), {:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.unfollow(followed, follower, id, false) do {:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
Actor.unfollow(follower, followed)
{:ok, activity, object} {:ok, activity, object}
else else
e -> e ->
Logger.error(inspect(e)) Logger.debug(inspect(e))
:error :error
end end
end end
@ -300,14 +366,14 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
with actor <- get_actor(data), with actor <- get_actor(data),
{:ok, %Actor{url: _actor_url}} <- Actors.get_actor_by_url(actor), {:ok, %Actor{url: _actor_url}} <- Actors.get_actor_by_url(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), {:ok, object} <- fetch_obj_helper(object_id),
# TODO : Validate that DELETE comes indeed form right domain (see above) # TODO : Validate that DELETE comes indeed form right domain (see above)
# :ok <- contain_origin(actor_url, object.data), # :ok <- contain_origin(actor_url, object.data),
{:ok, activity, object} <- ActivityPub.delete(object, false) do {:ok, activity, object} <- ActivityPub.delete(object, false) do
{:ok, activity, object} {:ok, activity, object}
else else
e -> e ->
Logger.error(inspect(e)) Logger.debug(inspect(e))
:error :error
end end
end end
@ -327,7 +393,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# ) do # ) do
# with actor <- get_actor(data), # with actor <- get_actor(data),
# %Actor{} = actor <- Actors.get_or_fetch_by_url(actor), # %Actor{} = actor <- Actors.get_or_fetch_by_url(actor),
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), # {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do # {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity} # {:ok, activity}
# else # else
@ -340,6 +406,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:error, :not_supported} {:error, :not_supported}
end end
defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <-
{:not_found, Actors.get_follow_by_url(follow_object_id)} do
{:ok, follow}
else
{:not_found, err} ->
{:error, "Follow URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Follow object"}
end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do
with false <- String.starts_with?(in_reply_to, "http"), with false <- String.starts_with?(in_reply_to, "http"),
{:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do {:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do
@ -523,50 +603,23 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# |> Map.put("attachment", attachments) # |> Map.put("attachment", attachments)
# end # end
@spec fetch_obj_helper(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()} @spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(url) when is_bitstring(url), do: ActivityPub.fetch_object_from_url(url) def fetch_obj_helper(object) do
Logger.debug("Fetching object #{inspect(object)}")
@spec fetch_obj_helper(map()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()} case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_url(obj["id"]) {:ok, object} ->
{:ok, object}
@spec get_obj_helper(String.t()) :: {:ok, struct()} | nil err ->
def get_obj_helper(id) do Logger.info("Error while fetching #{inspect(object)}")
if object = normalize(id), do: {:ok, object}, else: nil {:error, err}
end
@spec normalize(map()) :: struct() | nil
def normalize(obj) when is_map(obj), do: get_anything_by_url(obj["id"])
@spec normalize(String.t()) :: struct() | nil
def normalize(url) when is_binary(url), do: get_anything_by_url(url)
@spec normalize(any()) :: nil
def normalize(_), do: nil
@spec normalize(String.t()) :: struct() | nil
def get_anything_by_url(url) do
Logger.debug(fn -> "Getting anything from url #{url}" end)
get_actor_url(url) || get_event_url(url) || get_comment_url(url)
end
defp get_actor_url(url) do
case Actors.get_actor_by_url(url) do
{:ok, %Actor{} = actor} -> actor
_ -> nil
end end
end end
defp get_event_url(url) do def fetch_obj_helper_as_activity_streams(object) do
case Events.get_event_by_url(url) do with {:ok, object} <- fetch_obj_helper(object) do
{:ok, %Event{} = event} -> event {:ok, Mobilizon.Service.ActivityPub.Convertible.model_to_as(object)}
_ -> nil
end
end
defp get_comment_url(url) do
case Events.get_comment_full_from_url(url) do
{:ok, %Comment{} = comment} -> comment
_ -> nil
end end
end end
end end

View file

@ -30,12 +30,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
def get_url(object) do def get_url(%{"id" => id}), do: id
case object do def get_url(id) when is_bitstring(id), do: id
%{"id" => id} -> id def get_url(_), do: nil
id -> id
end
end
def make_json_ld_header do def make_json_ld_header do
%{ %{
@ -150,7 +147,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
else else
err -> err ->
Logger.error("Error while inserting a remote comment inside database") Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err)) Logger.debug(inspect(err))
{:error, err} {:error, err}
end end
end end
@ -172,7 +169,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
else else
err -> err ->
Logger.error("Error while inserting a remote comment inside database") Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err)) Logger.debug(inspect(err))
{:error, err} {:error, err}
end end
end end
@ -463,61 +460,98 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"object" => followed_id "object" => followed_id
} }
data =
if activity_id,
do: Map.put(data, "id", activity_id),
else: data
Logger.debug(inspect(data)) Logger.debug(inspect(data))
if activity_id, data
do: Map.put(data, "id", activity_id),
else: data
end end
#### Announce-related helpers #### Announce-related helpers
require Logger
@doc """ @doc """
Make announce activity data for the given actor and object Make announce activity data for the given actor and object
""" """
def make_announce_data(actor, object, activity_id, public \\ true)
def make_announce_data( def make_announce_data(
%Actor{url: actor_url} = actor, %Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
%Event{url: event_url} = object, %{"id" => url, "type" => type} = _object,
activity_id activity_id,
) do public
)
when type in ["Group", "Person", "Application"] do
do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
end
def make_announce_data(
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
%{"id" => url, "type" => type, "actor" => object_actor_url} = _object,
activity_id,
public
)
when type in ["Note", "Event"] do
do_make_announce_data(
actor_url,
actor_followers_url,
object_actor_url,
url,
activity_id,
public
)
end
defp do_make_announce_data(
actor_url,
actor_followers_url,
object_actor_url,
object_url,
activity_id,
public \\ true
) do
{to, cc} =
if public do
{[actor_followers_url, object_actor_url],
["https://www.w3.org/ns/activitystreams#Public"]}
else
{[actor_followers_url], []}
end
data = %{ data = %{
"type" => "Announce", "type" => "Announce",
"actor" => actor_url, "actor" => actor_url,
"object" => event_url, "object" => object_url,
"to" => [actor.followers_url, object.actor.url], "to" => to,
"cc" => ["https://www.w3.org/ns/activitystreams#Public"] "cc" => cc
# "context" => object.data["context"]
} }
if activity_id, do: Map.put(data, "id", activity_id), else: data if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
@doc """ @doc """
Make announce activity data for the given actor and object Make unannounce activity data for the given actor and object
""" """
def make_announce_data( def make_unannounce_data(
%Actor{url: actor_url} = actor, %Actor{url: url} = actor,
%Comment{url: comment_url} = object, activity,
activity_id activity_id
) do ) do
data = %{ data = %{
"type" => "Announce", "type" => "Undo",
"actor" => actor_url, "actor" => url,
"object" => comment_url, "object" => activity,
"to" => [actor.followers_url, object.actor.url], "to" => [actor.followers_url, actor.url],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"] "cc" => ["https://www.w3.org/ns/activitystreams#Public"]
# "context" => object.data["context"]
} }
if activity_id, do: Map.put(data, "id", activity_id), else: data if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
#### Unfollow-related helpers #### Unfollow-related helpers
@spec make_unfollow_data(Actor.t(), Actor.t(), map(), String.t()) :: map() @spec make_unfollow_data(Actor.t(), Actor.t(), map(), String.t()) :: map()
@ -553,7 +587,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"to" => params.to |> Enum.uniq(), "to" => params.to |> Enum.uniq(),
"actor" => params.actor.url, "actor" => params.actor.url,
"object" => params.object, "object" => params.object,
"published" => published "published" => published,
"id" => params.object["id"] <> "/activity"
} }
|> Map.merge(additional) |> Map.merge(additional)
end end

View file

@ -0,0 +1,21 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/visibility.ex
defmodule Mobilizon.Service.ActivityPub.Visibility do
@moduledoc """
Utility functions related to content visibility
"""
alias Mobilizon.Activity
alias Mobilizon.Events.Event
@public "https://www.w3.org/ns/activitystreams#Public"
@spec is_public?(Activity.t() | map()) :: boolean()
def is_public?(%{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(data) when is_map(data), do: @public in (data["to"] ++ (data["cc"] || []))
def is_public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}")
end

View file

@ -58,10 +58,11 @@ defmodule Mobilizon.Service.Federator do
%Activity{} -> %Activity{} ->
Logger.info("Already had #{params["id"]}") Logger.info("Already had #{params["id"]}")
_e -> e ->
# Just drop those for now # Just drop those for now
Logger.error("Unhandled activity") Logger.error("Unhandled activity")
Logger.error(Jason.encode!(params)) Logger.debug(inspect(e))
Logger.debug(Jason.encode!(params))
end end
end end
@ -75,7 +76,7 @@ defmodule Mobilizon.Service.Federator do
end end
def enqueue(type, payload, priority \\ 1) do def enqueue(type, payload, priority \\ 1) do
Logger.debug("enqueue") Logger.debug("enqueue something with type #{inspect(type)}")
if Mix.env() == :test do if Mix.env() == :test do
handle(type, payload) handle(type, payload)
@ -111,7 +112,7 @@ defmodule Mobilizon.Service.Federator do
end end
def handle_cast(m, state) do def handle_cast(m, state) do
Logger.error(fn -> Logger.debug(fn ->
"Unknown: #{inspect(m)}, #{inspect(state)}" "Unknown: #{inspect(m)}, #{inspect(state)}"
end) end)

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.Service.Formatter do
""" """
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Service.HTML
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@ -87,8 +88,8 @@ defmodule Mobilizon.Service.Formatter do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end
def html_escape(_text, "text/html") do def html_escape(text, "text/html") do
# HTML.filter_tags(text) HTML.filter_tags(text)
end end
def html_escape(text, "text/plain") do def html_escape(text, "text/plain") do

73
lib/service/html.ex Normal file
View file

@ -0,0 +1,73 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/html.ex
defmodule Mobilizon.Service.HTML do
@moduledoc """
Service to filter tags out of HTML content
"""
alias HtmlSanitizeEx.Scrubber
alias Mobilizon.Service.HTML.Scrubber.Default
def filter_tags(html), do: Scrubber.scrub(html, Default)
end
defmodule Mobilizon.Service.HTML.Scrubber.Default do
@moduledoc "Custom strategy to filter HTML content"
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
# credo:disable-for-previous-line
# No idea how to fix this one…
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], ["https", "http"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", [])
Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("code", [])
Meta.allow_tag_with_these_attributes("del", [])
Meta.allow_tag_with_these_attributes("em", [])
Meta.allow_tag_with_these_attributes("i", [])
Meta.allow_tag_with_these_attributes("li", [])
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", [])
Meta.allow_tag_with_these_attributes("h4", [])
Meta.allow_tag_with_these_attributes("h5", [])
Meta.strip_everything_not_covered()
end

View file

@ -1,123 +0,0 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/http_signatures/http_signatures.ex
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Mobilizon.Service.HTTPSignatures do
@moduledoc """
# HTTP Signatures
Generates and checks HTTP Signatures
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
require Logger
def split_signature(sig) do
default = %{"headers" => "date"}
sig =
sig
|> String.trim()
|> String.split(",")
|> Enum.reduce(default, fn part, acc ->
[key | rest] = String.split(part, "=")
value = Enum.join(rest, "=")
Map.put(acc, key, String.trim(value, "\""))
end)
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
end
def validate(headers, signature, public_key) do
sigstring = build_signing_string(headers, signature["headers"])
Logger.debug(fn ->
"Signature: #{signature["signature"]}"
end)
Logger.debug(fn ->
"Sigstring: #{sigstring}"
end)
{:ok, sig} = Base.decode64(signature["signature"])
:public_key.verify(sigstring, :sha256, sig, public_key)
end
def validate_conn(conn) do
# TODO: How to get the right key and see if it is actually valid for that request.
# For now, fetch the key for the actor.
case conn.params["actor"] |> Actor.get_public_key_for_url() do
{:ok, public_key} ->
if validate_conn(conn, public_key) do
true
Logger.info("Could not validate request, re-fetching user and trying one more time")
# Fetch user anew and try one more time
with actor_id <- conn.params["actor"],
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- actor_id |> Actor.get_public_key_for_url() do
validate_conn(conn, public_key)
end
end
e ->
Logger.debug("Could not found url for actor!")
Logger.debug(inspect(e))
false
end
end
def validate_conn(conn, public_key) do
headers = Enum.into(conn.req_headers, %{})
host_without_port = String.split(headers["host"], ":") |> hd
headers = Map.put(headers, "host", host_without_port)
signature = split_signature(headers["signature"])
validate(headers, signature, public_key)
end
def build_signing_string(headers, used_headers) do
used_headers
|> Enum.map(fn header -> "#{header}: #{headers[header]}" end)
|> Enum.join("\n")
end
def sign(%Actor{} = actor, headers) do
with sigstring <- build_signing_string(headers, Map.keys(headers)),
{:ok, key} <- actor.keys |> Actor.prepare_public_key(),
signature <- sigstring |> :public_key.sign(:sha256, key) |> Base.encode64() do
[
keyId: actor.url <> "#main-key",
algorithm: "rsa-sha256",
headers: headers |> Map.keys() |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
else
err ->
Logger.error("Unable to sign headers")
Logger.error(inspect(err))
nil
end
end
def generate_date_header(date \\ Timex.now("GMT")) do
case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do
{:ok, date} ->
date
{:error, err} ->
Logger.error("Unable to generate date header")
Logger.error(inspect(err))
nil
end
end
def generate_request_target(method, path), do: "#{method} #{path}"
def build_digest(body) do
"SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
end
end

View file

@ -0,0 +1,83 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/signature.ex
defmodule Mobilizon.Service.HTTPSignatures.Signature do
@moduledoc """
Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures
"""
@behaviour HTTPSignatures.Adapter
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
require Logger
def key_id_to_actor_url(key_id) do
uri =
URI.parse(key_id)
|> Map.put(:fragment, nil)
uri =
if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do
Map.put(uri, :path, String.replace(uri.path, "/publickey", ""))
else
uri
end
URI.to_string(uri)
end
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Refetching public key for #{actor_id}"),
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%Actor{} = actor, headers) do
Logger.debug("Signing on behalf of #{actor.url}")
Logger.debug("headers")
Logger.debug(inspect(headers))
with {:ok, key} <- actor.keys |> Actor.prepare_public_key() do
HTTPSignatures.sign(key, actor.url <> "#main-key", headers)
end
end
def generate_date_header(date \\ Timex.now("GMT")) do
case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do
{:ok, date} ->
date
{:error, err} ->
Logger.error("Unable to generate date header")
Logger.debug(inspect(err))
nil
end
end
def generate_request_target(method, path), do: "#{method} #{path}"
def build_digest(body) do
"SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
end
end

View file

@ -93,6 +93,10 @@ defmodule Mobilizon.Mixfile do
{:auto_linker, {:auto_linker,
git: "https://git.pleroma.social/pleroma/auto_linker.git", git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "95e8188490e97505c56636c1379ffdf036c1fdde"}, ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
{:http_signatures,
git: "https://git.pleroma.social/pleroma/http_signatures.git",
ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
{:html_sanitize_ex, "~> 1.3.0"},
# Dev and test dependencies # Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_reload, "~> 1.2", only: :dev},
{:ex_machina, "~> 2.3", only: [:dev, :test]}, {:ex_machina, "~> 2.3", only: [:dev, :test]},
@ -279,7 +283,7 @@ defmodule Mobilizon.Mixfile do
MobilizonWeb.HTTPSignaturePlug, MobilizonWeb.HTTPSignaturePlug,
MobilizonWeb.WebFingerController, MobilizonWeb.WebFingerController,
MobilizonWeb.NodeInfoController, MobilizonWeb.NodeInfoController,
Mobilizon.Service.HTTPSignatures, Mobilizon.Service.HTTPSignatures.Signature,
Mobilizon.Service.WebFinger, Mobilizon.Service.WebFinger,
Mobilizon.Service.XmlBuilder, Mobilizon.Service.XmlBuilder,
Mobilizon.Service.Federator Mobilizon.Service.Federator

View file

@ -56,7 +56,9 @@
"guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"guardian_db": {:hex, :guardian_db, "2.0.1", "e62e383197e957cb9c6683926d45056ab814eb0362e3de7f65d4619ae19544e8", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "guardian_db": {:hex, :guardian_db, "2.0.1", "e62e383197e957cb9c6683926d45056ab814eb0362e3de7f65d4619ae19544e8", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "http_sign": {:hex, :http_sign, "0.1.1", "b16edb83aa282892f3271f9a048c155e772bf36e15700ab93901484c55f8dd10", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"icalendar": {:git, "https://github.com/tcitworld/icalendar.git", "bd08e872c125f70a87c3ac7d87ea2f22a5577059", []}, "icalendar": {:git, "https://github.com/tcitworld/icalendar.git", "bd08e872c125f70a87c3ac7d87ea2f22a5577059", []},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
@ -73,6 +75,7 @@
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"mmdb2_decoder": {:hex, :mmdb2_decoder, "1.1.0", "2e2347521bb3bf6b81b9ee58d3be2199cb68ea42dcbafcd0d8eb40214d2844cf", [:mix], [], "hexpm"}, "mmdb2_decoder": {:hex, :mmdb2_decoder, "1.1.0", "2e2347521bb3bf6b81b9ee58d3be2199cb68ea42dcbafcd0d8eb40214d2844cf", [:mix], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.7.2", "4d00b60288e338028e2af4cccff9b0da365d83b7e5da52e58fb2de513ef5fedd", [:mix], [], "hexpm"}, "mogrify": {:hex, :mogrify, "0.7.2", "4d00b60288e338028e2af4cccff9b0da365d83b7e5da52e58fb2de513ef5fedd", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},

View file

@ -0,0 +1,21 @@
defmodule Mobilizon.Repo.Migrations.AddUrlToFollowsAndMoveToUuid do
use Ecto.Migration
def up do
alter table(:followers, primary_key: false) do
remove(:score)
remove(:id)
add(:id, :uuid, primary_key: true)
add(:url, :string, null: false)
end
end
def down do
alter table(:followers, primary_key: true) do
add(:score, :integer, default: 1000)
remove(:id)
add(:id, :serial, primary_key: true)
remove(:url)
end
end
end

View file

@ -1,5 +1,5 @@
# source: http://localhost:4000/api # source: http://localhost:4000/api
# timestamp: Mon Jul 29 2019 15:24:10 GMT+0200 (GMT+02:00) # timestamp: Wed Aug 07 2019 17:57:34 GMT+0200 (GMT+02:00)
schema { schema {
query: RootQueryType query: RootQueryType
@ -119,6 +119,7 @@ type Address {
"""The address's street name (with number)""" """The address's street name (with number)"""
street: String street: String
url: String
} }
input AddressInput { input AddressInput {
@ -138,6 +139,7 @@ input AddressInput {
"""The address's street name (with number)""" """The address's street name (with number)"""
street: String street: String
url: String
} }
"""A comment""" """A comment"""
@ -688,7 +690,7 @@ type RootMutationType {
"""Create an event""" """Create an event"""
createEvent( createEvent(
beginsOn: DateTime! beginsOn: DateTime!
category: String! category: String
description: String! description: String!
endsOn: DateTime endsOn: DateTime
onlineAddress: String onlineAddress: String

View file

@ -0,0 +1,34 @@
{
"type": "Accept",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "rBzK4Kqhd4g7HDS8WE5oRbWQb2R+HF/6awbUuMWhgru/xCODT0SJWSri0qWqEO4fPcpoUyz2d25cw6o+iy9wiozQb3hQNnu69AR+H5Mytc06+g10KCHexbGhbAEAw/7IzmeXELHUbaqeduaDIbdt1zw4RkwLXdqgQcGXTJ6ND1wM3WMHXQCK1m0flasIXFoBxpliPAGiElV8s0+Ltuh562GvflG3kB3WO+j+NaR0ZfG5G9N88xMj9UQlCKit5gpAE5p6syUsCU2WGBHywTumv73i3OVTIFfq+P9AdMsRuzw1r7zoKEsthW4aOzLQDi01ZjvdBz8zH6JnjDU7SMN/Ig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T14:36:41Z"
},
"object": {
"type": "Follow",
"object": "http://mobilizon.test/@thomas0",
"actor": "http://mobilizon.test/@thomas1",
"id": "http://mobilizon.test/follows/fdfds"
},
"nickname": "lain",
"id": "\"id\": \"http://mobilizon.test/accepts/follows/fdfds",
"actor": "http://mobilizon.test/@thomas0",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View file

@ -10,14 +10,14 @@
"created": "2018-02-17T19:39:15Z" "created": "2018-02-17T19:39:15Z"
}, },
"published": "2018-02-17T19:39:15Z", "published": "2018-02-17T19:39:15Z",
"object": "https://social.tcit.fr/@tcit/101188891162897047", "object": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400",
"id": "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity", "id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
"cc": [ "cc": [
"https://social.tcit.fr/users/tcit", "https://framapiaf.org/users/Framasoft",
"https://social.tcit.fr/users/tcit/followers" "https://framapiaf.org/users/Framasoft/followers"
], ],
"atomUri": "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity", "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
"actor": "https://social.tcit.fr/users/tcit", "actor": "https://framapiaf.org/users/Framasoft",
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1", "https://w3id.org/security/v1",

View file

@ -0,0 +1,34 @@
{
"type": "Reject",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "rBzK4Kqhd4g7HDS8WE5oRbWQb2R+HF/6awbUuMWhgru/xCODT0SJWSri0qWqEO4fPcpoUyz2d25cw6o+iy9wiozQb3hQNnu69AR+H5Mytc06+g10KCHexbGhbAEAw/7IzmeXELHUbaqeduaDIbdt1zw4RkwLXdqgQcGXTJ6ND1wM3WMHXQCK1m0flasIXFoBxpliPAGiElV8s0+Ltuh562GvflG3kB3WO+j+NaR0ZfG5G9N88xMj9UQlCKit5gpAE5p6syUsCU2WGBHywTumv73i3OVTIFfq+P9AdMsRuzw1r7zoKEsthW4aOzLQDi01ZjvdBz8zH6JnjDU7SMN/Ig==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T14:36:41Z"
},
"object": {
"type": "Follow",
"object": "http://mastodon.example.org/users/admin",
"id": "http://localtesting.pleroma.lol/users/lain#follows/4",
"actor": "http://localtesting.pleroma.lol/users/lain"
},
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#rejects/follows/4",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

View file

@ -12,17 +12,17 @@
"http://www.w3.org/ns/activitystreams#Public" "http://www.w3.org/ns/activitystreams#Public"
], ],
"published": "2018-05-11T16:23:37Z", "published": "2018-05-11T16:23:37Z",
"object": "http://mastodon.example.org/@admin/99541947525187367", "object": "https://framapiaf.org/@Framasoft/statuses/102501959686438400",
"id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity", "id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
"cc": [ "cc": [
"http://mastodon.example.org/users/admin", "https://framapiaf.org/users/Framasoft/",
"http://mastodon.example.org/users/admin/followers" "https://framapiaf.org/users/Framasoft/followers"
], ],
"atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity", "atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
"actor": "http://mastodon.example.org/users/admin" "actor": "https://framapiaf.org/users/Framasoft"
}, },
"id": "http://mastodon.example.org/users/admin#announces/100011594053806179/undo", "id": "https://framapiaf.org/users/Framasoft#announces/100011594053806179/undo",
"actor": "http://mastodon.example.org/users/admin", "actor": "https://framapiaf.org/users/Framasoft",
"@context": [ "@context": [
"http://www.w3.org/ns/activitystreams", "http://www.w3.org/ns/activitystreams",
"http://w3id.org/security/v1", "http://w3id.org/security/v1",

View file

@ -0,0 +1,57 @@
[
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true"
},
"request_body": "",
"url": "http://localhost:8080/actor"
},
"response": {
"binary": false,
"body": "{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"endpoints\": {\"sharedInbox\": \"http://localhost:8080/inbox\"}, \"followers\": \"http://localhost:8080/followers\", \"following\": \"http://localhost:8080/following\", \"inbox\": \"http://localhost:8080/inbox\", \"name\": \"ActivityRelay\", \"type\": \"Application\", \"id\": \"http://localhost:8080/actor\", \"publicKey\": {\"id\": \"http://localhost:8080/actor#main-key\", \"owner\": \"http://localhost:8080/actor\", \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvs6UuAo26Sb3BiOK7xay\\nsBzqvXI3xd55JAP0pAk2faF+Vl3r67/g9MoND96JqCVMuzSJZ9oSsqa6ilJCxG3p\\nXUUfQUvqAMGW49cCvga86DG17Ennjbc4C6WIQtoW3Wm5OdDciPY2Dx+pSXdTOajB\\nFX6RHUZcgqHENrsm3jPZI138e/2OJeqdxv4/5t2xdPXEpWdPGitX9AJhrqPY4lzg\\nzQ9Y9wS2eS1CVL9vZZRf9Z4RiZvAfVb0s1iS/IUxrf4TYERRFJxEoDLD2SZVrkq6\\nvhGldCfw2ZnfTftA1ToXguC9S6nSaz+li0ajNjpK/xjZjlKvn0I078UPPe5LUlsb\\nUcYZvBx5PC5rV8yKMLlgxnTY8PqC8LEVc453wO7Ai4M5TeB0SUyEycZHSyLfvQXV\\nThEN/07u1UaJViY3U5S/SihyoCQUfJXQ3jx2SjGgM32/aJ3IwxgveLaTsaZ0VVKM\\nbawEFw6iAcWYM06hZSB6j6dkL1xh+FYGEQTPMYMqUOJi2r1cD8yMLe8dTFOmwMLt\\nBnf7xxvnjKJcv3e9zGRWIdLkQbBQn3BEuRTCUMgljipxdjbeE5/JSP1kQLB94ncb\\nb9gvYgtemJKvT8m37+HOi9MI4BMIlDwpRWjqPZmkNvkegR/1KPjJSsyAnGdd89ne\\np442vUqPyXIq0tSCDmjmU+cCAwEAAQ==\\n-----END PUBLIC KEY-----\"}, \"summary\": \"ActivityRelay bot\", \"preferredUsername\": \"relay\", \"url\": \"http://localhost:8080/actor\"}",
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": "1368",
"Date": "Thu, 01 Aug 2019 14:44:38 GMT",
"Server": "Python/3.7 aiohttp/3.3.2"
},
"status_code": 200,
"type": "ok"
}
},
{
"request": {
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/69/activity\",\"object\":\"http://localhost:8080/actor\",\"to\":[\"http://localhost:8080/actor\"],\"type\":\"Follow\"}",
"headers": {
"Content-Type": "application/activity+json",
"signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"UADlb5eaeqmujO5zGfK1mWB3WZFXU6lkUgSvEf5YyQMOIkMaudDwTfNPIa4IYh2VMLwyYSjOOXxkcBdCw4f9UnMBQBhomPNRNkJ0QBzoxILPmyxddAojH9IzwwAUL/nHSGWaO116bkCux0OcEM5AVIrCT6dENep39lOjnOGPelBB5mKMS78AxH4pU/5tTGFKmNgiRL4Q06ezPUJHKauRrMwzcqZYdjUn+U9MDBDrYyfAzqQlgBPU/fMCjwusndxaICb9c+40YE3WaXzKewIivfrMoOBzWyw6ZsgAG8/NoOH+8z9Z+hBvdjCUXeG2bvAPPclNkSJillwIA2PnMOVgpw==\"",
"digest": "SHA-256=Ady0Dj2bEXe201P9bThLaj1Kw/7O1cfrjN9IifEfVBg=",
"date": "Thu, 01 Aug 2019 14:44:38 GMT"
},
"method": "post",
"options": {
"pool": "default"
},
"request_body": "",
"url": "http://localhost:8080/inbox"
},
"response": {
"binary": false,
"body": "signature check failed, signature did not match key",
"headers": {
"Content-Length": "51",
"Content-Type": "text/plain; charset=utf-8",
"Date": "Thu, 01 Aug 2019 14:44:38 GMT",
"Server": "Python/3.7 aiohttp/3.3.2"
},
"status_code": 401,
"type": "ok"
}
}
]

View file

@ -0,0 +1,57 @@
[
{
"request": {
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.github.io/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"category\":\"sc:category\",\"sc\":\"http://schema.org#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/relay\",\"cc\":[\"https://www.w3.org/ns/activitystreams#Public\"],\"id\":\"http://mobilizon.test/follow/68/activity\",\"object\":\"http://localhost:8080/actor\",\"to\":[\"http://localhost:8080/actor\"],\"type\":\"Follow\"}",
"headers": {
"Content-Type": "application/activity+json",
"signature": "keyId=\"http://mobilizon.test/relay#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"WsxzipdObXsApVtY5l2yTonTOPV888XLKK2+AMQRyiNZm4RGMEux8kgBKgJIODaKmRx9EsX8dIzBtTmJdLyj5gqfjvGVyj8hVeR0ERNMZmjngh5EZ3W+ySbkdFYZeYDWhwpL1i+7dTFJ3zE/ASZVaTMeIgqEpFnzHNbamwPzBZVvcnzyraB1rrmwcbzzrk3UPlJ3tA+Xz67Njr2wOiNNsjZ53abArKZB3KGbife6OyrVrKldJ+UKZS+vokgUXFwvMBZxfdmH2GD+yXHPhCIu7bVu77ASdW7bl7tM3uIV/c/Wemy5qJtPOupwbDvpLZ9ETE5IRCoUPdQ7l75kvevNxQ==\"",
"digest": "SHA-256=qIEgTH6kBorFchTiX2kxd7onyZ7BHhvLgCODLs6RAVc=",
"date": "Thu, 01 Aug 2019 14:44:37 GMT"
},
"method": "post",
"options": {
"pool": "default"
},
"request_body": "",
"url": "http://localhost:8080/inbox"
},
"response": {
"binary": false,
"body": "signature check failed, signature did not match key",
"headers": {
"Content-Length": "51",
"Content-Type": "text/plain; charset=utf-8",
"Date": "Thu, 01 Aug 2019 14:44:37 GMT",
"Server": "Python/3.7 aiohttp/3.3.2"
},
"status_code": 401,
"type": "ok"
}
},
{
"request": {
"body": "",
"headers": {
"Accept": "application/activity+json"
},
"method": "get",
"options": {
"follow_redirect": "true"
},
"request_body": "",
"url": "http://localhost:8080/actor"
},
"response": {
"binary": false,
"body": "{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"endpoints\": {\"sharedInbox\": \"http://localhost:8080/inbox\"}, \"followers\": \"http://localhost:8080/followers\", \"following\": \"http://localhost:8080/following\", \"inbox\": \"http://localhost:8080/inbox\", \"name\": \"ActivityRelay\", \"type\": \"Application\", \"id\": \"http://localhost:8080/actor\", \"publicKey\": {\"id\": \"http://localhost:8080/actor#main-key\", \"owner\": \"http://localhost:8080/actor\", \"publicKeyPem\": \"-----BEGIN PUBLIC KEY-----\\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvs6UuAo26Sb3BiOK7xay\\nsBzqvXI3xd55JAP0pAk2faF+Vl3r67/g9MoND96JqCVMuzSJZ9oSsqa6ilJCxG3p\\nXUUfQUvqAMGW49cCvga86DG17Ennjbc4C6WIQtoW3Wm5OdDciPY2Dx+pSXdTOajB\\nFX6RHUZcgqHENrsm3jPZI138e/2OJeqdxv4/5t2xdPXEpWdPGitX9AJhrqPY4lzg\\nzQ9Y9wS2eS1CVL9vZZRf9Z4RiZvAfVb0s1iS/IUxrf4TYERRFJxEoDLD2SZVrkq6\\nvhGldCfw2ZnfTftA1ToXguC9S6nSaz+li0ajNjpK/xjZjlKvn0I078UPPe5LUlsb\\nUcYZvBx5PC5rV8yKMLlgxnTY8PqC8LEVc453wO7Ai4M5TeB0SUyEycZHSyLfvQXV\\nThEN/07u1UaJViY3U5S/SihyoCQUfJXQ3jx2SjGgM32/aJ3IwxgveLaTsaZ0VVKM\\nbawEFw6iAcWYM06hZSB6j6dkL1xh+FYGEQTPMYMqUOJi2r1cD8yMLe8dTFOmwMLt\\nBnf7xxvnjKJcv3e9zGRWIdLkQbBQn3BEuRTCUMgljipxdjbeE5/JSP1kQLB94ncb\\nb9gvYgtemJKvT8m37+HOi9MI4BMIlDwpRWjqPZmkNvkegR/1KPjJSsyAnGdd89ne\\np442vUqPyXIq0tSCDmjmU+cCAwEAAQ==\\n-----END PUBLIC KEY-----\"}, \"summary\": \"ActivityRelay bot\", \"preferredUsername\": \"relay\", \"url\": \"http://localhost:8080/actor\"}",
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": "1368",
"Date": "Thu, 01 Aug 2019 14:44:36 GMT",
"Server": "Python/3.7 aiohttp/3.3.2"
},
"status_code": 200,
"type": "ok"
}
}
]

View file

@ -479,9 +479,9 @@ defmodule Mobilizon.ActorsTest do
alias Mobilizon.Actors.Follower alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
@valid_attrs %{approved: true, score: 42} @valid_attrs %{approved: true}
@update_attrs %{approved: false, score: 43} @update_attrs %{approved: false}
@invalid_attrs %{approved: nil, score: nil} @invalid_attrs %{approved: nil}
setup do setup do
actor = insert(:actor) actor = insert(:actor)
@ -509,7 +509,6 @@ defmodule Mobilizon.ActorsTest do
assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs) assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs)
assert follower.approved == true assert follower.approved == true
assert follower.score == 42
assert %{total: 1, elements: [target_actor]} = Actor.get_followings(actor) assert %{total: 1, elements: [target_actor]} = Actor.get_followings(actor)
assert %{total: 1, elements: [actor]} = Actor.get_followers(target_actor) assert %{total: 1, elements: [actor]} = Actor.get_followers(target_actor)
@ -546,7 +545,6 @@ defmodule Mobilizon.ActorsTest do
assert {:ok, follower} = Actors.update_follower(follower, @update_attrs) assert {:ok, follower} = Actors.update_follower(follower, @update_attrs)
assert %Follower{} = follower assert %Follower{} = follower
assert follower.approved == false assert follower.approved == false
assert follower.score == 43
end end
test "update_follower/2 with invalid data returns error changeset", context do test "update_follower/2 with invalid data returns error changeset", context do
@ -582,12 +580,12 @@ defmodule Mobilizon.ActorsTest do
assert actor.followings |> Enum.map(& &1.target_actor_id) == [target_actor.id] assert actor.followings |> Enum.map(& &1.target_actor_id) == [target_actor.id]
# Test if actor is already following target actor # Test if actor is already following target actor
{:error, msg} = Actor.follow(target_actor, actor) assert {:error, :already_following, msg} = Actor.follow(target_actor, actor)
assert msg =~ "already following" assert msg =~ "already following"
# Test if target actor is suspended # Test if target actor is suspended
target_actor = %{target_actor | suspended: true} target_actor = %{target_actor | suspended: true}
{:error, msg} = Actor.follow(target_actor, actor) assert {:error, :suspended, msg} = Actor.follow(target_actor, actor)
assert msg =~ "suspended" assert msg =~ "suspended"
end end
end end

View file

@ -11,7 +11,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Service.HTTPSignatures alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
@ -24,12 +24,12 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
actor = insert(:actor) actor = insert(:actor)
signature = signature =
HTTPSignatures.sign(actor, %{ Signature.sign(actor, %{
host: "example.com", host: "example.com",
"content-length": 15, "content-length": 15,
digest: Jason.encode!(%{id: "my_id"}) |> HTTPSignatures.build_digest(), digest: Jason.encode!(%{id: "my_id"}) |> Signature.build_digest(),
"(request-target)": HTTPSignatures.generate_request_target("POST", "/inbox"), "(request-target)": Signature.generate_request_target("POST", "/inbox"),
date: HTTPSignatures.generate_date_header() date: Signature.generate_date_header()
}) })
assert signature =~ "headers=\"(request-target) content-length date digest host\"" assert signature =~ "headers=\"(request-target) content-length date digest host\""
@ -53,21 +53,21 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
end end
describe "create activities" do describe "create activities" do
test "removes doubled 'to' recipients" do # test "removes doubled 'to' recipients" do
actor = insert(:actor) # actor = insert(:actor)
#
{:ok, activity, _} = # {:ok, activity, _} =
ActivityPub.create(%{ # ActivityPub.create(%{
to: ["user1", "user1", "user2"], # to: ["user1", "user1", "user2"],
actor: actor, # actor: actor,
context: "", # context: "",
object: %{} # object: %{}
}) # })
#
assert activity.data["to"] == ["user1", "user2"] # assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == actor.url # assert activity.actor == actor.url
assert activity.recipients == ["user1", "user2"] # assert activity.recipients == ["user1", "user2"]
end # end
end end
describe "fetching an" do describe "fetching an" do
@ -110,6 +110,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
end end
describe "deletion" do describe "deletion" do
# TODO: The delete activity it relayed and fetched once again (and then not found /o\)
test "it creates a delete activity and deletes the original event" do test "it creates a delete activity and deletes the original event" do
event = insert(:event) event = insert(:event)
event = Events.get_event_full_by_url!(event.url) event = Events.get_event_full_by_url!(event.url)

View file

@ -0,0 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mobilizon.Service.ActivityPub.RelayTest do
use Mobilizon.DataCase
alias Mobilizon.Service.ActivityPub.Relay
test "gets an actor for the relay" do
actor = Relay.get_actor()
assert actor.url =~ "/relay"
end
end

View file

@ -13,6 +13,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event} alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.ActivityPub.Transmogrifier
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
@ -151,7 +152,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
assert Enum.at(data["object"]["tag"], 2) == "moo" assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo"
end end
# test "it works for incoming notices with contentMap" do # test "it works for incoming notices with contentMap" do
@ -293,43 +294,41 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
# assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2" # assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
# end # end
# test "it works for incoming announces" do test "it works for incoming announces" do
# data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
# assert data["actor"] == "https://social.tcit.fr/users/tcit" assert data["actor"] == "https://framapiaf.org/users/Framasoft"
# assert data["type"] == "Announce" assert data["type"] == "Announce"
# assert data["id"] == assert data["id"] ==
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity" "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
# assert data["object"] == assert data["object"] ==
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047" "https://framapiaf.org/users/Framasoft/statuses/102501959686438400"
# assert %Comment{} = Events.get_comment_from_url(data["object"]) assert %Comment{} = Events.get_comment_from_url(data["object"])
# end end
# test "it works for incoming announces with an existing activity" do test "it works for incoming announces with an existing activity" do
# comment = insert(:comment) comment = insert(:comment)
# data = data =
# File.read!("test/fixtures/mastodon-announce.json") File.read!("test/fixtures/mastodon-announce.json")
# |> Jason.decode!() |> Jason.decode!()
# |> Map.put("object", comment.url) |> Map.put("object", comment.url)
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
# assert data["actor"] == "https://social.tcit.fr/users/tcit" assert data["actor"] == "https://framapiaf.org/users/Framasoft"
# assert data["type"] == "Announce" assert data["type"] == "Announce"
# assert data["id"] == assert data["id"] ==
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity" "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
# assert data["object"] == comment.url assert data["object"] == comment.url
end
# # assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
# end
test "it works for incoming update activities" do test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
@ -423,32 +422,32 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
# assert Repo.get(Activity, activity.id) # assert Repo.get(Activity, activity.id)
# end # end
# test "it works for incoming unannounces with an existing notice" do test "it works for incoming unannounces with an existing notice" do
# comment = insert(:comment) comment = insert(:comment)
# announce_data = announce_data =
# File.read!("test/fixtures/mastodon-announce.json") File.read!("test/fixtures/mastodon-announce.json")
# |> Jason.decode!() |> Jason.decode!()
# |> Map.put("object", comment.url) |> Map.put("object", comment.url)
# {:ok, %Activity{data: announce_data, local: false}} = {:ok, %Activity{data: announce_data, local: false}, _} =
# Transmogrifier.handle_incoming(announce_data) Transmogrifier.handle_incoming(announce_data)
# data = data =
# File.read!("test/fixtures/mastodon-undo-announce.json") File.read!("test/fixtures/mastodon-undo-announce.json")
# |> Jason.decode!() |> Jason.decode!()
# |> Map.put("object", announce_data) |> Map.put("object", announce_data)
# |> Map.put("actor", announce_data["actor"]) |> Map.put("actor", announce_data["actor"])
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
# assert data["type"] == "Undo" assert data["type"] == "Undo"
# assert data["object"]["type"] == "Announce" assert data["object"]["type"] == "Announce"
# assert data["object"]["object"] == comment.url assert data["object"]["object"] == comment.url
# assert data["object"]["id"] == assert data["object"]["id"] ==
# "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
# end end
test "it works for incomming unfollows with an existing follow" do test "it works for incomming unfollows with an existing follow" do
actor = insert(:actor) actor = insert(:actor)
@ -552,175 +551,127 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
# refute User.blocks?(blocker, user) # refute User.blocks?(blocker, user)
# end # end
# test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which were pre-accepted" do
# follower = insert(:user) follower = insert(:actor)
# followed = insert(:user) followed = insert(:actor)
# {:ok, follower} = User.follow(follower, followed) refute Actor.following?(follower, followed)
# assert User.following?(follower, followed) == true
# {:ok, follow_activity} = ActivityPub.follow(follower, followed) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
assert Actor.following?(follower, followed)
# accept_data = accept_data =
# File.read!("test/fixtures/mastodon-accept-activity.json") File.read!("test/fixtures/mastodon-accept-activity.json")
# |> Jason.decode!() |> Jason.decode!()
# |> Map.put("actor", followed.ap_id) |> Map.put("actor", followed.url)
# object = object =
# accept_data["object"] accept_data["object"]
# |> Map.put("actor", follower.ap_id) |> Map.put("actor", follower.url)
# |> Map.put("id", follow_activity.data["id"]) |> Map.put("id", follow_activity.data["id"])
# accept_data = Map.put(accept_data, "object", object) accept_data = Map.put(accept_data, "object", object)
# {:ok, activity} = Transmogrifier.handle_incoming(accept_data) {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
# refute activity.local refute activity.local
# assert activity.data["object"] == follow_activity.data["id"] assert activity.data["object"]["id"] == follow_activity.data["id"]
# follower = Repo.get(User, follower.id) {:ok, follower} = Actors.get_actor_by_url(follower.url)
# assert User.following?(follower, followed) == true assert Actor.following?(follower, followed)
# end end
# test "it works for incoming accepts which were orphaned" do test "it works for incoming accepts which are referenced by IRI only" do
# follower = insert(:user) follower = insert(:actor)
# followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:actor)
# {:ok, follow_activity} = ActivityPub.follow(follower, followed) {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
# accept_data = accept_data =
# File.read!("test/fixtures/mastodon-accept-activity.json") File.read!("test/fixtures/mastodon-accept-activity.json")
# |> Jason.decode!() |> Jason.decode!()
# |> Map.put("actor", followed.ap_id) |> Map.put("actor", followed.url)
|> Map.put("object", follow_activity.data["id"])
# accept_data = {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) assert activity.data["object"] == follow_activity.data["id"]
assert activity.data["object"] =~ "/follow/"
assert activity.data["id"] =~ "/accept/follow/"
# {:ok, activity} = Transmogrifier.handle_incoming(accept_data) {:ok, follower} = Actors.get_actor_by_url(follower.url)
# assert activity.data["object"] == follow_activity.data["id"]
# follower = Repo.get(User, follower.id) assert Actor.following?(follower, followed)
end
# assert User.following?(follower, followed) == true test "it fails for incoming accepts which cannot be correlated" do
# end follower = insert(:actor)
followed = insert(:actor)
# test "it works for incoming accepts which are referenced by IRI only" do accept_data =
# follower = insert(:user) File.read!("test/fixtures/mastodon-accept-activity.json")
# followed = insert(:user, %{info: %{"locked" => true}}) |> Jason.decode!()
|> Map.put("actor", followed.url)
# {:ok, follow_activity} = ActivityPub.follow(follower, followed) accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
# accept_data = :error = Transmogrifier.handle_incoming(accept_data)
# File.read!("test/fixtures/mastodon-accept-activity.json")
# |> Jason.decode!()
# |> Map.put("actor", followed.ap_id)
# |> Map.put("object", follow_activity.data["id"])
# {:ok, activity} = Transmogrifier.handle_incoming(accept_data) {:ok, follower} = Actors.get_actor_by_url(follower.url)
# assert activity.data["object"] == follow_activity.data["id"]
# follower = Repo.get(User, follower.id) refute Actor.following?(follower, followed)
end
# assert User.following?(follower, followed) == true test "it fails for incoming rejects which cannot be correlated" do
# end follower = insert(:actor)
followed = insert(:actor)
# test "it fails for incoming accepts which cannot be correlated" do accept_data =
# follower = insert(:user) File.read!("test/fixtures/mastodon-reject-activity.json")
# followed = insert(:user, %{info: %{"locked" => true}}) |> Jason.decode!()
|> Map.put("actor", followed.url)
# accept_data = accept_data =
# File.read!("test/fixtures/mastodon-accept-activity.json") Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
# |> Jason.decode!()
# |> Map.put("actor", followed.ap_id)
# accept_data = :error = Transmogrifier.handle_incoming(accept_data)
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
# :error = Transmogrifier.handle_incoming(accept_data) {:ok, follower} = Actors.get_actor_by_url(follower.url)
# follower = Repo.get(User, follower.id) refute Actor.following?(follower, followed)
end
# refute User.following?(follower, followed) == true test "it works for incoming rejects which are referenced by IRI only" do
# end follower = insert(:actor)
followed = insert(:actor)
# test "it fails for incoming rejects which cannot be correlated" do {:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
# follower = insert(:user)
# followed = insert(:user, %{info: %{"locked" => true}})
# accept_data = assert Actor.following?(follower, followed)
# File.read!("test/fixtures/mastodon-reject-activity.json")
# |> Jason.decode!()
# |> Map.put("actor", followed.ap_id)
# accept_data = reject_data =
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) File.read!("test/fixtures/mastodon-reject-activity.json")
|> Jason.decode!()
|> Map.put("actor", followed.url)
|> Map.put("object", follow_activity.data["id"])
# :error = Transmogrifier.handle_incoming(accept_data) {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
# follower = Repo.get(User, follower.id) refute Actor.following?(follower, followed)
end
# refute User.following?(follower, followed) == true test "it rejects activities without a valid ID" do
# end actor = insert(:actor)
# test "it works for incoming rejects which are orphaned" do data =
# follower = insert(:user) File.read!("test/fixtures/mastodon-follow-activity.json")
# followed = insert(:user, %{info: %{"locked" => true}}) |> Jason.decode!()
|> Map.put("object", actor.url)
|> Map.put("id", "")
# {:ok, follower} = User.follow(follower, followed) :error = Transmogrifier.handle_incoming(data)
# {:ok, _follow_activity} = ActivityPub.follow(follower, followed) end
# assert User.following?(follower, followed) == true
# reject_data =
# File.read!("test/fixtures/mastodon-reject-activity.json")
# |> Jason.decode!()
# |> Map.put("actor", followed.ap_id)
# reject_data =
# Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
# {:ok, activity} = Transmogrifier.handle_incoming(reject_data)
# refute activity.local
# follower = Repo.get(User, follower.id)
# assert User.following?(follower, followed) == false
# end
# test "it works for incoming rejects which are referenced by IRI only" do
# follower = insert(:user)
# followed = insert(:user, %{info: %{"locked" => true}})
# {:ok, follower} = User.follow(follower, followed)
# {:ok, follow_activity} = ActivityPub.follow(follower, followed)
# assert User.following?(follower, followed) == true
# reject_data =
# File.read!("test/fixtures/mastodon-reject-activity.json")
# |> Jason.decode!()
# |> Map.put("actor", followed.ap_id)
# |> Map.put("object", follow_activity.data["id"])
# {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
# follower = Repo.get(User, follower.id)
# assert User.following?(follower, followed) == false
# end
# test "it rejects activities without a valid ID" do
# user = insert(:user)
# data =
# File.read!("test/fixtures/mastodon-follow-activity.json")
# |> Jason.decode!()
# |> Map.put("object", user.ap_id)
# |> Map.put("id", "")
# :error = Transmogrifier.handle_incoming(data)
# end
test "it accepts Flag activities" do test "it accepts Flag activities" do
%Actor{url: reporter_url} = _reporter = insert(:actor) %Actor{url: reporter_url} = _reporter = insert(:actor)

View file

@ -279,6 +279,28 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
end end
end end
describe "/relay" do
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
Mobilizon.CommonConfig.put([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
|> assert
Mobilizon.CommonConfig.put([:instance, :allow_relay], true)
end
end
# #
# describe "/@:preferred_username/following" do # describe "/@:preferred_username/following" do
# test "it returns the following in a collection", %{conn: conn} do # test "it returns the following in a collection", %{conn: conn} do

View file

@ -39,6 +39,7 @@ defmodule Mobilizon.Factory do
url: Actor.build_url(preferred_username, :page), url: Actor.build_url(preferred_username, :page),
followers_url: Actor.build_url(preferred_username, :followers), followers_url: Actor.build_url(preferred_username, :followers),
following_url: Actor.build_url(preferred_username, :following), following_url: Actor.build_url(preferred_username, :following),
inbox_url: Actor.build_url(preferred_username, :inbox),
outbox_url: Actor.build_url(preferred_username, :outbox), outbox_url: Actor.build_url(preferred_username, :outbox),
user: nil user: nil
} }
@ -54,9 +55,13 @@ defmodule Mobilizon.Factory do
end end
def follower_factory do def follower_factory do
uuid = Ecto.UUID.generate()
%Mobilizon.Actors.Follower{ %Mobilizon.Actors.Follower{
target_actor: build(:actor), target_actor: build(:actor),
actor: build(:actor) actor: build(:actor),
id: uuid,
url: "#{MobilizonWeb.Endpoint.url()}/follows/#{uuid}"
} }
end end
@ -118,6 +123,7 @@ defmodule Mobilizon.Factory do
visibility: :public, visibility: :public,
tags: build_list(3, :tag), tags: build_list(3, :tag),
url: Routes.page_url(Endpoint, :event, uuid), url: Routes.page_url(Endpoint, :event, uuid),
picture: insert(:picture),
uuid: uuid uuid: uuid
} }
end end

47
test/tasks/relay_test.exs Normal file
View file

@ -0,0 +1,47 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Mobilizon.RelayTest do
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Actors
alias Mobilizon.Service.ActivityPub.Relay
use Mobilizon.DataCase
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
describe "running follow" do
test "relay is followed" do
use_cassette "relay/fetch_relay_follow" do
target_instance = "http://localhost:8080/actor"
Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance])
local_actor = Relay.get_actor()
assert local_actor.url =~ "/relay"
{:ok, target_actor} = Actors.get_actor_by_url(target_instance)
refute is_nil(target_actor.domain)
assert Actor.following?(local_actor, target_actor)
end
end
end
describe "running unfollow" do
test "relay is unfollowed" do
use_cassette "relay/fetch_relay_unfollow" do
target_instance = "http://localhost:8080/actor"
Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance])
%Actor{} = local_actor = Relay.get_actor()
{:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance)
assert %Follower{} = Actor.following?(local_actor, target_actor)
Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])
refute Actor.following?(local_actor, target_actor)
end
end
end
end