Merge branch 'feature/activity-pub-relay' into 'master'
Feature/activity pub relay See merge request framasoft/mobilizon!164
This commit is contained in:
commit
8fcd68faad
2
.gitignore
vendored
2
.gitignore
vendored
@ -16,7 +16,9 @@ erl_crash.dump
|
||||
/config/*.secret.exs
|
||||
|
||||
.env.production
|
||||
.env.test
|
||||
.env
|
||||
.env.2
|
||||
|
||||
setup_db.psql
|
||||
|
||||
|
@ -16,6 +16,7 @@ config :mobilizon, :instance,
|
||||
hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost",
|
||||
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
|
||||
repository: Mix.Project.config()[:source_url],
|
||||
allow_relay: true,
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 16_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, :json_library, Jason
|
||||
|
||||
config :http_signatures,
|
||||
adapter: Mobilizon.Service.HTTPSignatures.Signature
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
|
||||
endpoint:
|
||||
System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org",
|
||||
|
@ -11,7 +11,9 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
|
||||
],
|
||||
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,
|
||||
code_reloader: true,
|
||||
|
@ -56,6 +56,7 @@
|
||||
"chai": "^4.2.0",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
"eslint": "^6.0.1",
|
||||
"graphql-cli": "^3.0.12",
|
||||
"node-sass": "^4.11.0",
|
||||
"patch-package": "^6.1.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
|
@ -98,6 +98,7 @@ export default class App extends Vue {
|
||||
@import "~buefy/src/scss/components/tag";
|
||||
@import "~buefy/src/scss/components/taginput";
|
||||
@import "~buefy/src/scss/components/upload";
|
||||
@import "~buefy/src/scss/components/radio";
|
||||
|
||||
.router-enter-active,
|
||||
.router-leave-active {
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* Example: framameet.org
|
||||
*/
|
||||
export const MOBILIZON_INSTANCE_HOST = process.env.MOBILIZON_INSTANCE_HOST;
|
||||
export const MOBILIZON_INSTANCE_HOST = window.location.hostname;
|
||||
|
||||
/**
|
||||
* URL on which the API is. "/api" will be added at the end
|
||||
@ -14,7 +14,7 @@ export const MOBILIZON_INSTANCE_HOST = process.env.MOBILIZON_INSTANCE_HOST;
|
||||
*
|
||||
* Example: https://framameet.org
|
||||
*/
|
||||
export const GRAPHQL_API_ENDPOINT = process.env.GRAPHQL_API_ENDPOINT;
|
||||
export const GRAPHQL_API_ENDPOINT = window.location.origin;
|
||||
|
||||
/**
|
||||
* URL with path on which the API is. Replaces GRAPHQL_API_ENDPOINT if used
|
||||
@ -23,4 +23,4 @@ export const GRAPHQL_API_ENDPOINT = process.env.GRAPHQL_API_ENDPOINT;
|
||||
*
|
||||
* Example: https://framameet.org/api
|
||||
*/
|
||||
export const GRAPHQL_API_FULL_PATH = process.env.GRAPHQL_API_FULL_PATH;
|
||||
export const GRAPHQL_API_FULL_PATH = `${window.location.origin}/api`;
|
||||
|
21
js/src/components/Account/ActorLink.vue
Normal file
21
js/src/components/Account/ActorLink.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<span>
|
||||
<router-link v-if="actor.domain === null"
|
||||
:to="{name: 'Profile', params: { name: actor.preferredUsername } }"
|
||||
>
|
||||
<slot></slot>
|
||||
</router-link>
|
||||
<a v-else :href="actor.url">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IActor } from '@/types/actor';
|
||||
|
||||
@Component
|
||||
export default class ActorLink extends Vue {
|
||||
@Prop() actor!: IActor;
|
||||
}
|
||||
</script>
|
@ -51,6 +51,7 @@ export const FETCH_EVENT = gql`
|
||||
preferredUsername,
|
||||
domain,
|
||||
name,
|
||||
url,
|
||||
},
|
||||
# attributedTo {
|
||||
# avatar {
|
||||
@ -141,11 +142,12 @@ export const CREATE_EVENT = gql`
|
||||
$title: String!,
|
||||
$description: String!,
|
||||
$organizerActorId: ID!,
|
||||
$category: String!,
|
||||
$category: String,
|
||||
$beginsOn: DateTime!,
|
||||
$picture: PictureInput,
|
||||
$tags: [String],
|
||||
$physicalAddress: AddressInput!
|
||||
$physicalAddress: AddressInput,
|
||||
$visibility: EventVisibility
|
||||
) {
|
||||
createEvent(
|
||||
title: $title,
|
||||
@ -155,7 +157,8 @@ export const CREATE_EVENT = gql`
|
||||
category: $category,
|
||||
picture: $picture,
|
||||
tags: $tags,
|
||||
physicalAddress: $physicalAddress
|
||||
physicalAddress: $physicalAddress,
|
||||
visibility: $visibility
|
||||
) {
|
||||
id,
|
||||
uuid,
|
||||
@ -172,7 +175,7 @@ export const EDIT_EVENT = gql`
|
||||
$title: String!,
|
||||
$description: String!,
|
||||
$organizerActorId: Int!,
|
||||
$category: String!
|
||||
$category: String
|
||||
) {
|
||||
EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, category: $category) {
|
||||
uuid
|
||||
|
@ -53,7 +53,7 @@ export interface IEvent {
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
category: Category;
|
||||
category: Category|null;
|
||||
|
||||
beginsOn: Date;
|
||||
endsOn: Date;
|
||||
|
@ -6,6 +6,11 @@
|
||||
<div v-if="$apollo.loading">Loading...</div>
|
||||
<div class="columns is-centered" v-else>
|
||||
<form class="column is-two-thirds-desktop" @submit="createEvent">
|
||||
<h2 class="subtitle">
|
||||
<translate>
|
||||
General informations
|
||||
</translate>
|
||||
</h2>
|
||||
<picture-upload v-model="pictureFile" />
|
||||
|
||||
<b-field :label="$gettext('Title')">
|
||||
@ -24,7 +29,7 @@
|
||||
<editor v-model="event.description" />
|
||||
</div>
|
||||
|
||||
<b-field :label="$gettext('Category')">
|
||||
<!--<b-field :label="$gettext('Category')">
|
||||
<b-select placeholder="Select a category" v-model="event.category">
|
||||
<option
|
||||
v-for="category in categories"
|
||||
@ -32,7 +37,28 @@
|
||||
:key="category"
|
||||
>{{ $gettext(category) }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</b-field>-->
|
||||
|
||||
<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>
|
||||
@ -50,6 +76,7 @@ import {
|
||||
Category,
|
||||
IEvent,
|
||||
EventModel,
|
||||
EventVisibility,
|
||||
} from '@/types/event.model';
|
||||
import { LOGGED_PERSON } from '@/graphql/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
@ -76,9 +103,10 @@ export default class CreateEvent extends Vue {
|
||||
@Prop({ required: false, type: String }) uuid!: string;
|
||||
|
||||
loggedPerson: IPerson = new Person();
|
||||
categories: string[] = Object.keys(Category);
|
||||
/*categories: string[] = Object.keys(Category);*/
|
||||
event: IEvent = new EventModel();
|
||||
pictureFile: File | null = null;
|
||||
EventVisibility = EventVisibility;
|
||||
|
||||
created() {
|
||||
const now = new Date();
|
||||
|
@ -92,9 +92,7 @@
|
||||
</b-modal>
|
||||
</div>
|
||||
<div class="organizer">
|
||||
<router-link
|
||||
:to="{name: 'Profile', params: { name: event.organizerActor.preferredUsername } }"
|
||||
>
|
||||
<actor-link :actor="event.organizerActor">
|
||||
<translate
|
||||
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}"
|
||||
v-if="event.organizerActor">By %{ name }</translate>
|
||||
@ -104,7 +102,7 @@
|
||||
:src="event.organizerActor.avatar.url"
|
||||
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" />
|
||||
</figure>
|
||||
</router-link>
|
||||
</actor-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -242,9 +240,11 @@ import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
|
||||
import BIcon from 'buefy/src/components/icon/Icon.vue';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import EventFullDate from '@/components/Event/EventFullDate.vue';
|
||||
import ActorLink from '@/components/Account/ActorLink.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ActorLink,
|
||||
EventFullDate,
|
||||
EventCard,
|
||||
BIcon,
|
||||
|
@ -4,7 +4,7 @@ const path = require('path');
|
||||
module.exports = {
|
||||
pluginOptions: {
|
||||
webpackBundleAnalyzer: {
|
||||
openAnalyzer: false
|
||||
analyzerMode: 'disabled'
|
||||
}
|
||||
},
|
||||
lintOnSave: false,
|
||||
|
1928
js/yarn.lock
1928
js/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,11 @@ defmodule Mix.Tasks.Mobilizon.Common do
|
||||
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
|
||||
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
|
||||
end
|
||||
|
65
lib/mix/tasks/mobilizon/relay.ex
Normal file
65
lib/mix/tasks/mobilizon/relay.ex
Normal 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
|
@ -163,7 +163,6 @@ defmodule Mobilizon.Actors.Actor do
|
||||
])
|
||||
|> validate_required([
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:type,
|
||||
:domain,
|
||||
@ -184,6 +183,44 @@ defmodule Mobilizon.Actors.Actor do
|
||||
changes
|
||||
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 """
|
||||
Changeset for group creation
|
||||
"""
|
||||
@ -240,6 +277,14 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:outbox_url,
|
||||
build_url(username, :outbox)
|
||||
)
|
||||
|> put_change(
|
||||
:followers_url,
|
||||
build_url(username, :followers)
|
||||
)
|
||||
|> put_change(
|
||||
:following_url,
|
||||
build_url(username, :following)
|
||||
)
|
||||
|> put_change(
|
||||
:inbox_url,
|
||||
build_url(username, :inbox)
|
||||
@ -325,16 +370,28 @@ defmodule Mobilizon.Actors.Actor do
|
||||
%{total: Task.await(total), elements: Task.await(elements)}
|
||||
end
|
||||
|
||||
@spec get_full_followers(struct()) :: list()
|
||||
def get_full_followers(%Actor{id: actor_id} = _actor) do
|
||||
Repo.all(
|
||||
defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
)
|
||||
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 """
|
||||
@ -404,18 +461,19 @@ defmodule Mobilizon.Actors.Actor do
|
||||
Make an actor follow another
|
||||
"""
|
||||
@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},
|
||||
# Check if followed has blocked follower
|
||||
{:already_following, false} <- {:already_following, following?(follower, followed)} do
|
||||
do_follow(follower, followed, approved)
|
||||
do_follow(follower, followed, approved, url)
|
||||
else
|
||||
{:already_following, %Follower{}} ->
|
||||
{:error,
|
||||
{:error, :already_following,
|
||||
"Could not follow actor: you are already following #{followed.preferred_username}"}
|
||||
|
||||
{: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
|
||||
|
||||
@ -433,13 +491,20 @@ defmodule Mobilizon.Actors.Actor do
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_follow(struct(), struct(), boolean) ::
|
||||
@spec do_follow(struct(), struct(), boolean(), String.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(%{
|
||||
"actor_id" => follower.id,
|
||||
"target_actor_id" => followed.id,
|
||||
"approved" => approved
|
||||
"approved" => approved,
|
||||
"url" => url
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -297,7 +297,7 @@ defmodule Mobilizon.Actors do
|
||||
{:ok, actor}
|
||||
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
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()}
|
||||
def get_or_fetch_by_url(url, preload \\ false) do
|
||||
case get_actor_by_url(url, preload) do
|
||||
{:ok, actor} ->
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
case ActivityPub.make_actor_from_url(url, preload) do
|
||||
{:ok, actor} ->
|
||||
{:ok, %Actor{} = 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"}
|
||||
end
|
||||
end
|
||||
@ -655,6 +655,18 @@ defmodule Mobilizon.Actors do
|
||||
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
|
||||
|
||||
@doc """
|
||||
@ -895,7 +907,7 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
|
||||
@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()
|
||||
def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do
|
||||
@ -904,6 +916,19 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
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 """
|
||||
Creates a follower.
|
||||
|
||||
@ -1009,7 +1034,7 @@ defmodule Mobilizon.Actors do
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Error while removing an upload file")
|
||||
Logger.error(inspect(error))
|
||||
Logger.debug(inspect(error))
|
||||
{:ok, actor}
|
||||
end
|
||||
end
|
||||
|
@ -7,9 +7,11 @@ defmodule Mobilizon.Actors.Follower do
|
||||
alias Mobilizon.Actors.Follower
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
|
||||
schema "followers" do
|
||||
field(:approved, :boolean, default: false)
|
||||
field(:score, :integer, default: 1000)
|
||||
field(:url, :string)
|
||||
belongs_to(:target_actor, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
end
|
||||
@ -17,8 +19,34 @@ defmodule Mobilizon.Actors.Follower do
|
||||
@doc false
|
||||
def changeset(%Follower{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:score, :approved, :target_actor_id, :actor_id])
|
||||
|> validate_required([:score, :approved, :target_actor_id, :actor_id])
|
||||
|> cast(attrs, [:url, :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)
|
||||
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
|
||||
|
@ -91,7 +91,6 @@ defmodule Mobilizon.Events.Event do
|
||||
:title,
|
||||
:begins_on,
|
||||
:organizer_actor_id,
|
||||
:category,
|
||||
:url,
|
||||
:uuid
|
||||
])
|
||||
|
@ -32,7 +32,8 @@ defmodule Mobilizon.Events do
|
||||
:tracks,
|
||||
:tags,
|
||||
:participants,
|
||||
:physical_address
|
||||
:physical_address,
|
||||
:picture
|
||||
]
|
||||
)
|
||||
|> paginate(page, limit)
|
||||
@ -248,7 +249,8 @@ defmodule Mobilizon.Events do
|
||||
:tracks,
|
||||
:tags,
|
||||
:participants,
|
||||
:physical_address
|
||||
:physical_address,
|
||||
:picture
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -9,6 +9,8 @@ defmodule MobilizonWeb.API.Events do
|
||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||
alias MobilizonWeb.API.Utils
|
||||
|
||||
@visibility %{"PUBLIC" => :public, "PRIVATE" => :private}
|
||||
|
||||
@doc """
|
||||
Create an event
|
||||
"""
|
||||
|
51
lib/mobilizon_web/api/follows.ex
Normal file
51
lib/mobilizon_web/api/follows.ex
Normal 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
|
@ -9,7 +9,7 @@ defmodule MobilizonWeb.API.Utils do
|
||||
Determines the full audience based on mentions for a public audience
|
||||
|
||||
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
|
||||
"""
|
||||
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
|
||||
@ -72,7 +72,9 @@ defmodule MobilizonWeb.API.Utils do
|
||||
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
|
||||
# Actors.get(to)
|
||||
@ -138,7 +140,7 @@ defmodule MobilizonWeb.API.Utils do
|
||||
make_content_html(
|
||||
content,
|
||||
tags,
|
||||
"text/plain"
|
||||
"text/html"
|
||||
),
|
||||
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
|
||||
addressed_users <- get_addressed_users(mentioned_users, nil),
|
||||
|
@ -14,6 +14,19 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
|
||||
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
|
||||
with {page, ""} <- Integer.parse(page),
|
||||
%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
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||
Logger.debug("Got something with valid signature inside inbox")
|
||||
Federator.enqueue(:incoming_ap_doc, params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
@ -90,19 +104,35 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
"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
|
||||
|
||||
json(conn, "error")
|
||||
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
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json("Not found")
|
||||
end
|
||||
|
||||
def errors(conn, _e) do
|
||||
def errors(conn, e) do
|
||||
Logger.debug(inspect(e))
|
||||
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json("Unknown Error")
|
||||
|
@ -10,7 +10,6 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
|
||||
Plug to check HTTP Signatures on every incoming request
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.HTTPSignatures
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
@ -23,16 +22,11 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
|
||||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
actor = conn.params["actor"]
|
||||
|
||||
Logger.debug(fn ->
|
||||
"Checking sig for #{actor}"
|
||||
end)
|
||||
|
||||
[signature | _] = get_req_header(conn, "signature")
|
||||
|
||||
cond do
|
||||
String.contains?(signature, actor) ->
|
||||
if signature do
|
||||
# set (request-target) header to the appropriate value
|
||||
# we also replace the digest header with the one we computed
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
@ -40,13 +34,16 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
|
||||
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))
|
||||
|
||||
signature ->
|
||||
Logger.debug("Signature not from actor")
|
||||
assign(conn, :valid_signature, false)
|
||||
|
||||
true ->
|
||||
else
|
||||
Logger.debug("No signature header!")
|
||||
conn
|
||||
end
|
||||
|
@ -18,6 +18,10 @@ defmodule MobilizonWeb.Router do
|
||||
plug(MobilizonWeb.HTTPSignaturePlug)
|
||||
end
|
||||
|
||||
pipeline :relay do
|
||||
plug(:accepts, ["activity-json", "json"])
|
||||
end
|
||||
|
||||
pipeline :activity_pub do
|
||||
plug(:accepts, ["activity-json"])
|
||||
end
|
||||
@ -97,6 +101,13 @@ defmodule MobilizonWeb.Router do
|
||||
post("/inbox", ActivityPubController, :inbox)
|
||||
end
|
||||
|
||||
scope "/relay", MobilizonWeb do
|
||||
pipe_through(:relay)
|
||||
|
||||
get("/", ActivityPubController, :relay)
|
||||
post("/inbox", ActivityPubController, :inbox)
|
||||
end
|
||||
|
||||
scope "/proxy/", MobilizonWeb do
|
||||
pipe_through(:remote_media)
|
||||
|
||||
|
@ -131,7 +131,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
arg(:online_address, :string)
|
||||
arg(:phone_address, :string)
|
||||
arg(:organizer_actor_id, non_null(:id))
|
||||
arg(:category, non_null(:string))
|
||||
arg(:category, :string)
|
||||
arg(:physical_address, :address_input)
|
||||
|
||||
resolve(&Event.create_event/3)
|
||||
|
@ -12,12 +12,12 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
public_key = Mobilizon.Service.ActivityPub.Utils.pem_to_public_key_pem(actor.keys)
|
||||
|
||||
%{
|
||||
"id" => Actor.build_url(actor.preferred_username, :page),
|
||||
"type" => "Person",
|
||||
"following" => Actor.build_url(actor.preferred_username, :following),
|
||||
"followers" => Actor.build_url(actor.preferred_username, :followers),
|
||||
"inbox" => Actor.build_url(actor.preferred_username, :inbox),
|
||||
"outbox" => Actor.build_url(actor.preferred_username, :outbox),
|
||||
"id" => actor.url,
|
||||
"type" => to_string(actor.type),
|
||||
"following" => actor.following_url,
|
||||
"followers" => actor.followers_url,
|
||||
"inbox" => actor.inbox_url,
|
||||
"outbox" => actor.outbox_url,
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
|
@ -31,8 +31,8 @@ defmodule MobilizonWeb.ErrorView do
|
||||
# template is found, let's render it as 500
|
||||
def template_not_found(template, assigns) do
|
||||
require Logger
|
||||
Logger.error("Template not found")
|
||||
Logger.error(inspect(template))
|
||||
Logger.warn("Template not found")
|
||||
Logger.debug(inspect(template))
|
||||
render("500.html", assigns)
|
||||
end
|
||||
end
|
||||
|
@ -46,24 +46,8 @@ defmodule MobilizonWeb.PageView do
|
||||
end
|
||||
|
||||
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
|
||||
event = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
|
||||
{:ok, html, []} = Earmark.as_html(event["summary"])
|
||||
|
||||
%{
|
||||
"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"]
|
||||
}
|
||||
event
|
||||
|> Mobilizon.Service.ActivityPub.Converters.Event.model_to_as()
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
@ -21,10 +21,11 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
alias Mobilizon.Actors.Follower
|
||||
|
||||
alias Mobilizon.Service.Federator
|
||||
alias Mobilizon.Service.HTTPSignatures
|
||||
alias Mobilizon.Service.HTTPSignatures.Signature
|
||||
|
||||
require Logger
|
||||
import Mobilizon.Service.ActivityPub.Utils
|
||||
import Mobilizon.Service.ActivityPub.Visibility
|
||||
|
||||
@doc """
|
||||
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
|
||||
with map <- lazy_put_activity_defaults(map),
|
||||
{: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{
|
||||
data: map,
|
||||
local: local,
|
||||
@ -94,7 +91,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
|
||||
case data["type"] do
|
||||
"Event" ->
|
||||
{:ok, Events.get_event_by_url!(object_url)}
|
||||
{:ok, Events.get_event_full_by_url!(object_url)}
|
||||
|
||||
"Note" ->
|
||||
{:ok, Events.get_comment_full_from_url!(object_url)}
|
||||
@ -107,15 +104,17 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
end
|
||||
else
|
||||
{: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}} ->
|
||||
{: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)}
|
||||
|
||||
e ->
|
||||
require Logger
|
||||
Logger.error(inspect(e))
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
@ -137,24 +136,40 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
%{to: to, actor: actor, published: published, object: object},
|
||||
additional
|
||||
),
|
||||
:ok <- Logger.debug(inspect(create_data)),
|
||||
{:ok, activity, object} <- insert(create_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
# {:ok, actor} <- Actors.increase_event_count(actor) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong")
|
||||
Logger.error(inspect(err))
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
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
|
||||
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 <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
@ -168,6 +183,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
with data <- %{
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"id" => object["url"],
|
||||
"type" => "Update",
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
@ -215,55 +231,56 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# end
|
||||
# end
|
||||
|
||||
# def announce(
|
||||
# %Actor{} = actor,
|
||||
# object,
|
||||
# activity_id \\ nil,
|
||||
# local \\ true
|
||||
# ) do
|
||||
# #with true <- is_public?(object),
|
||||
# with announce_data <- make_announce_data(actor, object, activity_id),
|
||||
# {:ok, activity, object} <- insert(announce_data, local),
|
||||
# # {:ok, object} <- add_announce_to_object(activity, object),
|
||||
# :ok <- maybe_federate(activity) do
|
||||
# {:ok, activity, object}
|
||||
# else
|
||||
# error -> {:error, error}
|
||||
# end
|
||||
# end
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with true <- is_public?(object),
|
||||
announce_data <- make_announce_data(actor, object, activity_id, public),
|
||||
{:ok, activity, object} <- insert(announce_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
# def unannounce(
|
||||
# %Actor{} = actor,
|
||||
# object,
|
||||
# activity_id \\ nil,
|
||||
# local \\ true
|
||||
# ) do
|
||||
# with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
|
||||
# unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
||||
# {:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
|
||||
# :ok <- maybe_federate(unannounce_activity),
|
||||
# {:ok, _activity} <- Repo.delete(announce_activity),
|
||||
# {:ok, object} <- remove_announce_from_object(announce_activity, object) do
|
||||
# {:ok, unannounce_activity, object}
|
||||
# else
|
||||
# _e -> {:ok, object}
|
||||
# end
|
||||
# end
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
|
||||
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
||||
{:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
|
||||
:ok <- maybe_federate(unannounce_activity) do
|
||||
{:ok, unannounce_activity, object}
|
||||
else
|
||||
_e -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
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_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity",
|
||||
activity_id || follow_url,
|
||||
data <- make_follow_data(followed, follower, activity_follow_id),
|
||||
{:ok, activity, object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{err, _} when err in [:already_following, :suspended] ->
|
||||
{:error, err}
|
||||
{:error, err, msg} when err in [:already_following, :suspended] ->
|
||||
{:error, msg}
|
||||
end
|
||||
end
|
||||
|
||||
@ -271,18 +288,26 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
Make an actor unfollow another
|
||||
"""
|
||||
@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),
|
||||
# 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),
|
||||
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 <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
@ -294,7 +319,8 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"type" => "Delete",
|
||||
"actor" => actor.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),
|
||||
@ -309,6 +335,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
@ -324,6 +351,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"type" => "Delete",
|
||||
"actor" => url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
@ -366,11 +394,11 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
|
||||
# Request returned 410
|
||||
{:error, :actor_deleted} ->
|
||||
Logger.info("Actor was deleted")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
e ->
|
||||
Logger.error("Failed to make actor from url")
|
||||
Logger.error(inspect(e))
|
||||
Logger.warn("Failed to make actor from url")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
@ -414,10 +442,18 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"""
|
||||
def publish(actor, activity) do
|
||||
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 =
|
||||
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
|
||||
[]
|
||||
end
|
||||
@ -448,15 +484,16 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
Logger.info("Federating #{id} to #{inbox}")
|
||||
%URI{host: host, path: path} = URI.parse(inbox)
|
||||
|
||||
digest = HTTPSignatures.build_digest(json)
|
||||
date = HTTPSignatures.generate_date_header()
|
||||
request_target = HTTPSignatures.generate_request_target("POST", path)
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
HTTPSignatures.sign(actor, %{
|
||||
Signature.sign(actor, %{
|
||||
host: host,
|
||||
"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,
|
||||
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()
|
||||
defp fetch_and_prepare_actor_from_url(url) do
|
||||
Logger.debug("Fetching and preparing actor from url")
|
||||
Logger.debug(inspect(url))
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true),
|
||||
res =
|
||||
with %HTTPoison.Response{status_code: 200, body: body} <-
|
||||
HTTPoison.get!(url, [Accept: "application/activity+json"], follow_redirect: true),
|
||||
:ok <- Logger.debug("response okay, now decoding json"),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
actor_data_from_actor_object(data)
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
{:ok, %HTTPoison.Response{status_code: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
e ->
|
||||
Logger.error("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
e
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -7,3 +7,10 @@ defmodule Mobilizon.Service.ActivityPub.Converter do
|
||||
@callback as_to_model_data(map()) :: map()
|
||||
@callback model_to_as(struct()) :: map()
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Service.ActivityPub.Convertible do
|
||||
@type activitystreams :: map()
|
||||
|
||||
@spec model_to_as(t) :: activitystreams
|
||||
def model_to_as(convertible)
|
||||
end
|
||||
|
@ -45,3 +45,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
|
||||
}
|
||||
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
|
||||
|
@ -52,7 +52,29 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Address do
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(AddressModel.t()) :: map()
|
||||
def model_to_as(%AddressModel{} = _address) do
|
||||
nil
|
||||
def model_to_as(%AddressModel{} = address) do
|
||||
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
|
||||
|
@ -84,7 +84,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" => comment.actor.url,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => Routes.page_url(Endpoint, :comment, comment.uuid)
|
||||
"id" => comment.url
|
||||
}
|
||||
|
||||
if comment.in_reply_to_comment do
|
||||
@ -94,3 +94,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
|
||||
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
|
||||
|
@ -10,6 +10,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Service.ActivityPub.Converter
|
||||
alias Mobilizon.Service.ActivityPub.Converters.Address, as: AddressConverter
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Addresses
|
||||
@ -26,6 +28,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
@spec as_to_model_data(map()) :: map()
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("event as_to_model_data")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with {:actor, {:ok, %Actor{id: actor_id}}} <-
|
||||
{:actor, Actors.get_actor_by_url(object["actor"])},
|
||||
@ -99,6 +102,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
end
|
||||
|
||||
defp fetch_tags(tags) do
|
||||
Logger.debug("fetching tags")
|
||||
|
||||
Enum.reduce(tags, [], fn tag, acc ->
|
||||
with true <- tag["type"] == "Hashtag",
|
||||
{:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do
|
||||
@ -110,23 +115,62 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
|
||||
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 """
|
||||
Convert an event struct to an ActivityStream representation
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(EventModel.t()) :: map()
|
||||
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",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"title" => event.title,
|
||||
"to" => to,
|
||||
"cc" => [],
|
||||
"attributedTo" => event.organizer_actor.url,
|
||||
"name" => event.title,
|
||||
"actor" => event.organizer_actor.url,
|
||||
"uuid" => event.uuid,
|
||||
"category" => event.category,
|
||||
"summary" => event.description,
|
||||
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
|
||||
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
|
||||
"content" => event.description,
|
||||
"publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
88
lib/service/activity_pub/relay.ex
Normal file
88
lib/service/activity_pub/relay.ex
Normal 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
|
@ -8,11 +8,12 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Comment}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Service.ActivityPub.Visibility
|
||||
|
||||
require Logger
|
||||
|
||||
@ -45,7 +46,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
|> Map.put("actor", object["attributedTo"])
|
||||
|> fix_attachments
|
||||
|> fix_in_reply_to
|
||||
|> fix_tag
|
||||
|
||||
# |> fix_tag
|
||||
end
|
||||
|
||||
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)
|
||||
when not is_nil(in_reply_to) do
|
||||
Logger.error("inReplyTo ID seem incorrect")
|
||||
Logger.error(inspect(in_reply_to))
|
||||
Logger.warn("inReplyTo ID seem incorrect: #{inspect(in_reply_to)}")
|
||||
do_fix_in_reply_to("", object)
|
||||
end
|
||||
|
||||
@ -87,7 +88,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
object
|
||||
|
||||
e ->
|
||||
Logger.error("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
|
||||
Logger.warn("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
end
|
||||
@ -116,6 +117,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
|> Map.put("tag", combined)
|
||||
end
|
||||
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
||||
def handle_incoming(%{"type" => "Flag"} = data) do
|
||||
with params <- Mobilizon.Service.ActivityPub.Converters.Flag.as_to_model(data) do
|
||||
params = %{
|
||||
@ -186,13 +190,69 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
|
||||
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
|
||||
{: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}
|
||||
else
|
||||
e ->
|
||||
Logger.error("Unable to handle Follow activity")
|
||||
Logger.error(inspect(e))
|
||||
Logger.warn("Unable to handle Follow activity #{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
|
||||
end
|
||||
end
|
||||
@ -211,19 +271,21 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
# end
|
||||
# end
|
||||
# #
|
||||
# def handle_incoming(
|
||||
# %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||
# ) do
|
||||
# with actor <- get_actor(data),
|
||||
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
|
||||
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||
# {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
|
||||
# {:ok, activity}
|
||||
# else
|
||||
# e -> Logger.error(inspect e)
|
||||
# :error
|
||||
# end
|
||||
# end
|
||||
def handle_incoming(
|
||||
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
|
||||
public <- Visibility.is_public?(data),
|
||||
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
|
||||
@ -245,28 +307,33 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
})
|
||||
|
||||
e ->
|
||||
Logger.error(inspect(e))
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
# def handle_incoming(
|
||||
# %{
|
||||
# "type" => "Undo",
|
||||
# "object" => %{"type" => "Announce", "object" => object_id},
|
||||
# "actor" => actor,
|
||||
# "id" => id
|
||||
# } = data
|
||||
# ) do
|
||||
# with actor <- get_actor(data),
|
||||
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
|
||||
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||
# {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
|
||||
# {:ok, activity}
|
||||
# else
|
||||
# _e -> :error
|
||||
# end
|
||||
# end
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{
|
||||
"type" => "Announce",
|
||||
"object" => object_id,
|
||||
"id" => cancelled_activity_id
|
||||
},
|
||||
"actor" => actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
with actor <- get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
@ -278,12 +345,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
) do
|
||||
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
|
||||
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(followed, follower, id, false) do
|
||||
Actor.unfollow(follower, followed)
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.error(inspect(e))
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
@ -300,14 +366,14 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
|
||||
with actor <- get_actor(data),
|
||||
{: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)
|
||||
# :ok <- contain_origin(actor_url, object.data),
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
Logger.error(inspect(e))
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
@ -327,7 +393,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
# ) do
|
||||
# with actor <- get_actor(data),
|
||||
# %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}
|
||||
# else
|
||||
@ -340,6 +406,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
{:error, :not_supported}
|
||||
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
|
||||
with false <- String.starts_with?(in_reply_to, "http"),
|
||||
{: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)
|
||||
# end
|
||||
|
||||
@spec fetch_obj_helper(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
|
||||
def fetch_obj_helper(url) when is_bitstring(url), do: ActivityPub.fetch_object_from_url(url)
|
||||
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
|
||||
def fetch_obj_helper(object) do
|
||||
Logger.debug("Fetching object #{inspect(object)}")
|
||||
|
||||
@spec fetch_obj_helper(map()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
|
||||
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_url(obj["id"])
|
||||
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
|
||||
{:ok, object} ->
|
||||
{:ok, object}
|
||||
|
||||
@spec get_obj_helper(String.t()) :: {:ok, struct()} | nil
|
||||
def get_obj_helper(id) do
|
||||
if object = normalize(id), do: {:ok, object}, else: nil
|
||||
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
|
||||
err ->
|
||||
Logger.info("Error while fetching #{inspect(object)}")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_event_url(url) do
|
||||
case Events.get_event_by_url(url) do
|
||||
{:ok, %Event{} = event} -> event
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_comment_url(url) do
|
||||
case Events.get_comment_full_from_url(url) do
|
||||
{:ok, %Comment{} = comment} -> comment
|
||||
_ -> nil
|
||||
def fetch_obj_helper_as_activity_streams(object) do
|
||||
with {:ok, object} <- fetch_obj_helper(object) do
|
||||
{:ok, Mobilizon.Service.ActivityPub.Convertible.model_to_as(object)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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,
|
||||
# so figure out what the actor's URI is based on what we have.
|
||||
def get_url(object) do
|
||||
case object do
|
||||
%{"id" => id} -> id
|
||||
id -> id
|
||||
end
|
||||
end
|
||||
def get_url(%{"id" => id}), do: id
|
||||
def get_url(id) when is_bitstring(id), do: id
|
||||
def get_url(_), do: nil
|
||||
|
||||
def make_json_ld_header do
|
||||
%{
|
||||
@ -150,7 +147,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
else
|
||||
err ->
|
||||
Logger.error("Error while inserting a remote comment inside database")
|
||||
Logger.error(inspect(err))
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
@ -172,7 +169,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
else
|
||||
err ->
|
||||
Logger.error("Error while inserting a remote comment inside database")
|
||||
Logger.error(inspect(err))
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
@ -463,61 +460,98 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"object" => followed_id
|
||||
}
|
||||
|
||||
Logger.debug(inspect(data))
|
||||
|
||||
data =
|
||||
if activity_id,
|
||||
do: Map.put(data, "id", activity_id),
|
||||
else: data
|
||||
|
||||
Logger.debug(inspect(data))
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
#### Announce-related helpers
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Make announce activity data for the given actor and object
|
||||
"""
|
||||
def make_announce_data(actor, object, activity_id, public \\ true)
|
||||
|
||||
def make_announce_data(
|
||||
%Actor{url: actor_url} = actor,
|
||||
%Event{url: event_url} = object,
|
||||
activity_id
|
||||
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
|
||||
%{"id" => url, "type" => type} = _object,
|
||||
activity_id,
|
||||
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 = %{
|
||||
"type" => "Announce",
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"to" => [actor.followers_url, object.actor.url],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
# "context" => object.data["context"]
|
||||
"object" => object_url,
|
||||
"to" => to,
|
||||
"cc" => cc
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
end
|
||||
|
||||
@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(
|
||||
%Actor{url: actor_url} = actor,
|
||||
%Comment{url: comment_url} = object,
|
||||
def make_unannounce_data(
|
||||
%Actor{url: url} = actor,
|
||||
activity,
|
||||
activity_id
|
||||
) do
|
||||
data = %{
|
||||
"type" => "Announce",
|
||||
"actor" => actor_url,
|
||||
"object" => comment_url,
|
||||
"to" => [actor.followers_url, object.actor.url],
|
||||
"type" => "Undo",
|
||||
"actor" => url,
|
||||
"object" => activity,
|
||||
"to" => [actor.followers_url, actor.url],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
# "context" => object.data["context"]
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
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
|
||||
|
||||
@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(),
|
||||
"actor" => params.actor.url,
|
||||
"object" => params.object,
|
||||
"published" => published
|
||||
"published" => published,
|
||||
"id" => params.object["id"] <> "/activity"
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
21
lib/service/activity_pub/visibility.ex
Normal file
21
lib/service/activity_pub/visibility.ex
Normal 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
|
@ -58,10 +58,11 @@ defmodule Mobilizon.Service.Federator do
|
||||
%Activity{} ->
|
||||
Logger.info("Already had #{params["id"]}")
|
||||
|
||||
_e ->
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.error("Unhandled activity")
|
||||
Logger.error(Jason.encode!(params))
|
||||
Logger.debug(inspect(e))
|
||||
Logger.debug(Jason.encode!(params))
|
||||
end
|
||||
end
|
||||
|
||||
@ -75,7 +76,7 @@ defmodule Mobilizon.Service.Federator do
|
||||
end
|
||||
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
Logger.debug("enqueue")
|
||||
Logger.debug("enqueue something with type #{inspect(type)}")
|
||||
|
||||
if Mix.env() == :test do
|
||||
handle(type, payload)
|
||||
@ -111,7 +112,7 @@ defmodule Mobilizon.Service.Federator do
|
||||
end
|
||||
|
||||
def handle_cast(m, state) do
|
||||
Logger.error(fn ->
|
||||
Logger.debug(fn ->
|
||||
"Unknown: #{inspect(m)}, #{inspect(state)}"
|
||||
end)
|
||||
|
||||
|
@ -9,6 +9,7 @@ defmodule Mobilizon.Service.Formatter do
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Service.HTML
|
||||
|
||||
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||
@ -87,8 +88,8 @@ defmodule Mobilizon.Service.Formatter do
|
||||
{html_escape(text, type), mentions, hashtags}
|
||||
end
|
||||
|
||||
def html_escape(_text, "text/html") do
|
||||
# HTML.filter_tags(text)
|
||||
def html_escape(text, "text/html") do
|
||||
HTML.filter_tags(text)
|
||||
end
|
||||
|
||||
def html_escape(text, "text/plain") do
|
||||
|
73
lib/service/html.ex
Normal file
73
lib/service/html.ex
Normal 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
|
@ -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
|
83
lib/service/http_signatures/signature.ex
Normal file
83
lib/service/http_signatures/signature.ex
Normal 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
|
6
mix.exs
6
mix.exs
@ -93,6 +93,10 @@ defmodule Mobilizon.Mixfile do
|
||||
{:auto_linker,
|
||||
git: "https://git.pleroma.social/pleroma/auto_linker.git",
|
||||
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
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
||||
@ -279,7 +283,7 @@ defmodule Mobilizon.Mixfile do
|
||||
MobilizonWeb.HTTPSignaturePlug,
|
||||
MobilizonWeb.WebFingerController,
|
||||
MobilizonWeb.NodeInfoController,
|
||||
Mobilizon.Service.HTTPSignatures,
|
||||
Mobilizon.Service.HTTPSignatures.Signature,
|
||||
Mobilizon.Service.WebFinger,
|
||||
Mobilizon.Service.XmlBuilder,
|
||||
Mobilizon.Service.Federator
|
||||
|
3
mix.lock
3
mix.lock
@ -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_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"},
|
||||
"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_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"},
|
||||
"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"},
|
||||
@ -73,6 +75,7 @@
|
||||
"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"},
|
||||
"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"},
|
||||
"mogrify": {:hex, :mogrify, "0.7.2", "4d00b60288e338028e2af4cccff9b0da365d83b7e5da52e58fb2de513ef5fedd", [:mix], [], "hexpm"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
|
||||
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
# 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 {
|
||||
query: RootQueryType
|
||||
@ -119,6 +119,7 @@ type Address {
|
||||
|
||||
"""The address's street name (with number)"""
|
||||
street: String
|
||||
url: String
|
||||
}
|
||||
|
||||
input AddressInput {
|
||||
@ -138,6 +139,7 @@ input AddressInput {
|
||||
|
||||
"""The address's street name (with number)"""
|
||||
street: String
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A comment"""
|
||||
@ -688,7 +690,7 @@ type RootMutationType {
|
||||
"""Create an event"""
|
||||
createEvent(
|
||||
beginsOn: DateTime!
|
||||
category: String!
|
||||
category: String
|
||||
description: String!
|
||||
endsOn: DateTime
|
||||
onlineAddress: String
|
||||
|
34
test/fixtures/mastodon-accept-activity.json
vendored
Normal file
34
test/fixtures/mastodon-accept-activity.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
12
test/fixtures/mastodon-announce.json
vendored
12
test/fixtures/mastodon-announce.json
vendored
@ -10,14 +10,14 @@
|
||||
"created": "2018-02-17T19:39:15Z"
|
||||
},
|
||||
"published": "2018-02-17T19:39:15Z",
|
||||
"object": "https://social.tcit.fr/@tcit/101188891162897047",
|
||||
"id": "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity",
|
||||
"object": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400",
|
||||
"id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
|
||||
"cc": [
|
||||
"https://social.tcit.fr/users/tcit",
|
||||
"https://social.tcit.fr/users/tcit/followers"
|
||||
"https://framapiaf.org/users/Framasoft",
|
||||
"https://framapiaf.org/users/Framasoft/followers"
|
||||
],
|
||||
"atomUri": "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity",
|
||||
"actor": "https://social.tcit.fr/users/tcit",
|
||||
"atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
|
||||
"actor": "https://framapiaf.org/users/Framasoft",
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
|
34
test/fixtures/mastodon-reject-activity.json
vendored
Normal file
34
test/fixtures/mastodon-reject-activity.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
16
test/fixtures/mastodon-undo-announce.json
vendored
16
test/fixtures/mastodon-undo-announce.json
vendored
@ -12,17 +12,17 @@
|
||||
"http://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"published": "2018-05-11T16:23:37Z",
|
||||
"object": "http://mastodon.example.org/@admin/99541947525187367",
|
||||
"id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
|
||||
"object": "https://framapiaf.org/@Framasoft/statuses/102501959686438400",
|
||||
"id": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
|
||||
"cc": [
|
||||
"http://mastodon.example.org/users/admin",
|
||||
"http://mastodon.example.org/users/admin/followers"
|
||||
"https://framapiaf.org/users/Framasoft/",
|
||||
"https://framapiaf.org/users/Framasoft/followers"
|
||||
],
|
||||
"atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
|
||||
"actor": "http://mastodon.example.org/users/admin"
|
||||
"atomUri": "https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity",
|
||||
"actor": "https://framapiaf.org/users/Framasoft"
|
||||
},
|
||||
"id": "http://mastodon.example.org/users/admin#announces/100011594053806179/undo",
|
||||
"actor": "http://mastodon.example.org/users/admin",
|
||||
"id": "https://framapiaf.org/users/Framasoft#announces/100011594053806179/undo",
|
||||
"actor": "https://framapiaf.org/users/Framasoft",
|
||||
"@context": [
|
||||
"http://www.w3.org/ns/activitystreams",
|
||||
"http://w3id.org/security/v1",
|
||||
|
57
test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json
vendored
Normal file
57
test/fixtures/vcr_cassettes/relay/fetch_relay_follow.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
57
test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json
vendored
Normal file
57
test/fixtures/vcr_cassettes/relay/fetch_relay_unfollow.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
@ -479,9 +479,9 @@ defmodule Mobilizon.ActorsTest do
|
||||
alias Mobilizon.Actors.Follower
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@valid_attrs %{approved: true, score: 42}
|
||||
@update_attrs %{approved: false, score: 43}
|
||||
@invalid_attrs %{approved: nil, score: nil}
|
||||
@valid_attrs %{approved: true}
|
||||
@update_attrs %{approved: false}
|
||||
@invalid_attrs %{approved: nil}
|
||||
|
||||
setup do
|
||||
actor = insert(:actor)
|
||||
@ -509,7 +509,6 @@ defmodule Mobilizon.ActorsTest do
|
||||
|
||||
assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs)
|
||||
assert follower.approved == true
|
||||
assert follower.score == 42
|
||||
|
||||
assert %{total: 1, elements: [target_actor]} = Actor.get_followings(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 %Follower{} = follower
|
||||
assert follower.approved == false
|
||||
assert follower.score == 43
|
||||
end
|
||||
|
||||
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]
|
||||
|
||||
# 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"
|
||||
|
||||
# Test if target actor is suspended
|
||||
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"
|
||||
end
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Service.HTTPSignatures
|
||||
alias Mobilizon.Service.HTTPSignatures.Signature
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||
|
||||
@ -24,12 +24,12 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
|
||||
actor = insert(:actor)
|
||||
|
||||
signature =
|
||||
HTTPSignatures.sign(actor, %{
|
||||
Signature.sign(actor, %{
|
||||
host: "example.com",
|
||||
"content-length": 15,
|
||||
digest: Jason.encode!(%{id: "my_id"}) |> HTTPSignatures.build_digest(),
|
||||
"(request-target)": HTTPSignatures.generate_request_target("POST", "/inbox"),
|
||||
date: HTTPSignatures.generate_date_header()
|
||||
digest: Jason.encode!(%{id: "my_id"}) |> Signature.build_digest(),
|
||||
"(request-target)": Signature.generate_request_target("POST", "/inbox"),
|
||||
date: Signature.generate_date_header()
|
||||
})
|
||||
|
||||
assert signature =~ "headers=\"(request-target) content-length date digest host\""
|
||||
@ -53,21 +53,21 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
|
||||
end
|
||||
|
||||
describe "create activities" do
|
||||
test "removes doubled 'to' recipients" do
|
||||
actor = insert(:actor)
|
||||
|
||||
{:ok, activity, _} =
|
||||
ActivityPub.create(%{
|
||||
to: ["user1", "user1", "user2"],
|
||||
actor: actor,
|
||||
context: "",
|
||||
object: %{}
|
||||
})
|
||||
|
||||
assert activity.data["to"] == ["user1", "user2"]
|
||||
assert activity.actor == actor.url
|
||||
assert activity.recipients == ["user1", "user2"]
|
||||
end
|
||||
# test "removes doubled 'to' recipients" do
|
||||
# actor = insert(:actor)
|
||||
#
|
||||
# {:ok, activity, _} =
|
||||
# ActivityPub.create(%{
|
||||
# to: ["user1", "user1", "user2"],
|
||||
# actor: actor,
|
||||
# context: "",
|
||||
# object: %{}
|
||||
# })
|
||||
#
|
||||
# assert activity.data["to"] == ["user1", "user2"]
|
||||
# assert activity.actor == actor.url
|
||||
# assert activity.recipients == ["user1", "user2"]
|
||||
# end
|
||||
end
|
||||
|
||||
describe "fetching an" do
|
||||
@ -110,6 +110,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
|
||||
end
|
||||
|
||||
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
|
||||
event = insert(:event)
|
||||
event = Events.get_event_full_by_url!(event.url)
|
||||
|
15
test/mobilizon/service/activity_pub/relay_test.exs
Normal file
15
test/mobilizon/service/activity_pub/relay_test.exs
Normal 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
|
@ -13,6 +13,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Service.ActivityPub.Transmogrifier
|
||||
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!()
|
||||
|
||||
{: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
|
||||
|
||||
# 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"
|
||||
# end
|
||||
|
||||
# test "it works for incoming announces" do
|
||||
# data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
|
||||
test "it works for incoming announces" do
|
||||
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["type"] == "Announce"
|
||||
assert data["actor"] == "https://framapiaf.org/users/Framasoft"
|
||||
assert data["type"] == "Announce"
|
||||
|
||||
# assert data["id"] ==
|
||||
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity"
|
||||
assert data["id"] ==
|
||||
"https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
|
||||
|
||||
# assert data["object"] ==
|
||||
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047"
|
||||
assert data["object"] ==
|
||||
"https://framapiaf.org/users/Framasoft/statuses/102501959686438400"
|
||||
|
||||
# assert %Comment{} = Events.get_comment_from_url(data["object"])
|
||||
# end
|
||||
assert %Comment{} = Events.get_comment_from_url(data["object"])
|
||||
end
|
||||
|
||||
# test "it works for incoming announces with an existing activity" do
|
||||
# comment = insert(:comment)
|
||||
test "it works for incoming announces with an existing activity" do
|
||||
comment = insert(:comment)
|
||||
|
||||
# data =
|
||||
# File.read!("test/fixtures/mastodon-announce.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("object", comment.url)
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-announce.json")
|
||||
|> Jason.decode!()
|
||||
|> 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["type"] == "Announce"
|
||||
assert data["actor"] == "https://framapiaf.org/users/Framasoft"
|
||||
assert data["type"] == "Announce"
|
||||
|
||||
# assert data["id"] ==
|
||||
# "https://social.tcit.fr/users/tcit/statuses/101188891162897047/activity"
|
||||
assert data["id"] ==
|
||||
"https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
|
||||
|
||||
# assert data["object"] == comment.url
|
||||
|
||||
# # assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
|
||||
# end
|
||||
assert data["object"] == comment.url
|
||||
end
|
||||
|
||||
test "it works for incoming update activities" do
|
||||
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)
|
||||
# end
|
||||
|
||||
# test "it works for incoming unannounces with an existing notice" do
|
||||
# comment = insert(:comment)
|
||||
test "it works for incoming unannounces with an existing notice" do
|
||||
comment = insert(:comment)
|
||||
|
||||
# announce_data =
|
||||
# File.read!("test/fixtures/mastodon-announce.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("object", comment.url)
|
||||
announce_data =
|
||||
File.read!("test/fixtures/mastodon-announce.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", comment.url)
|
||||
|
||||
# {:ok, %Activity{data: announce_data, local: false}} =
|
||||
# Transmogrifier.handle_incoming(announce_data)
|
||||
{:ok, %Activity{data: announce_data, local: false}, _} =
|
||||
Transmogrifier.handle_incoming(announce_data)
|
||||
|
||||
# data =
|
||||
# File.read!("test/fixtures/mastodon-undo-announce.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("object", announce_data)
|
||||
# |> Map.put("actor", announce_data["actor"])
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-undo-announce.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", announce_data)
|
||||
|> 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["object"]["type"] == "Announce"
|
||||
# assert data["object"]["object"] == comment.url
|
||||
assert data["type"] == "Undo"
|
||||
assert data["object"]["type"] == "Announce"
|
||||
assert data["object"]["object"] == comment.url
|
||||
|
||||
# assert data["object"]["id"] ==
|
||||
# "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
|
||||
# end
|
||||
assert data["object"]["id"] ==
|
||||
"https://framapiaf.org/users/Framasoft/statuses/102501959686438400/activity"
|
||||
end
|
||||
|
||||
test "it works for incomming unfollows with an existing follow" do
|
||||
actor = insert(:actor)
|
||||
@ -552,175 +551,127 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||
# refute User.blocks?(blocker, user)
|
||||
# end
|
||||
|
||||
# test "it works for incoming accepts which were pre-accepted" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user)
|
||||
test "it works for incoming accepts which were pre-accepted" do
|
||||
follower = insert(:actor)
|
||||
followed = insert(:actor)
|
||||
|
||||
# {:ok, follower} = User.follow(follower, followed)
|
||||
# assert User.following?(follower, followed) == true
|
||||
refute Actor.following?(follower, followed)
|
||||
|
||||
# {:ok, follow_activity} = ActivityPub.follow(follower, followed)
|
||||
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
||||
assert Actor.following?(follower, followed)
|
||||
|
||||
# accept_data =
|
||||
# File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("actor", followed.ap_id)
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("actor", followed.url)
|
||||
|
||||
# object =
|
||||
# accept_data["object"]
|
||||
# |> Map.put("actor", follower.ap_id)
|
||||
# |> Map.put("id", follow_activity.data["id"])
|
||||
object =
|
||||
accept_data["object"]
|
||||
|> Map.put("actor", follower.url)
|
||||
|> 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)
|
||||
# refute activity.local
|
||||
{:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
|
||||
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
|
||||
# end
|
||||
assert Actor.following?(follower, followed)
|
||||
end
|
||||
|
||||
# test "it works for incoming accepts which were orphaned" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user, %{info: %{"locked" => true}})
|
||||
test "it works for incoming accepts which are referenced by IRI only" do
|
||||
follower = insert(:actor)
|
||||
followed = insert(:actor)
|
||||
|
||||
# {:ok, follow_activity} = ActivityPub.follow(follower, followed)
|
||||
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
||||
|
||||
# accept_data =
|
||||
# File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("actor", followed.ap_id)
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("actor", followed.url)
|
||||
|> Map.put("object", follow_activity.data["id"])
|
||||
|
||||
# accept_data =
|
||||
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
|
||||
{:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
|
||||
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)
|
||||
# assert activity.data["object"] == follow_activity.data["id"]
|
||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||
|
||||
# follower = Repo.get(User, follower.id)
|
||||
assert Actor.following?(follower, followed)
|
||||
end
|
||||
|
||||
# assert User.following?(follower, followed) == true
|
||||
# end
|
||||
test "it fails for incoming accepts which cannot be correlated" do
|
||||
follower = insert(:actor)
|
||||
followed = insert(:actor)
|
||||
|
||||
# test "it works for incoming accepts which are referenced by IRI only" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user, %{info: %{"locked" => true}})
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
|> 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 =
|
||||
# File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("actor", followed.ap_id)
|
||||
# |> Map.put("object", follow_activity.data["id"])
|
||||
:error = Transmogrifier.handle_incoming(accept_data)
|
||||
|
||||
# {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
|
||||
# assert activity.data["object"] == follow_activity.data["id"]
|
||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||
|
||||
# follower = Repo.get(User, follower.id)
|
||||
refute Actor.following?(follower, followed)
|
||||
end
|
||||
|
||||
# assert User.following?(follower, followed) == true
|
||||
# end
|
||||
test "it fails for incoming rejects which cannot be correlated" do
|
||||
follower = insert(:actor)
|
||||
followed = insert(:actor)
|
||||
|
||||
# test "it fails for incoming accepts which cannot be correlated" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user, %{info: %{"locked" => true}})
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-reject-activity.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("actor", followed.url)
|
||||
|
||||
# accept_data =
|
||||
# File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("actor", followed.ap_id)
|
||||
accept_data =
|
||||
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.url))
|
||||
|
||||
# accept_data =
|
||||
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
|
||||
:error = Transmogrifier.handle_incoming(accept_data)
|
||||
|
||||
# :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
|
||||
# end
|
||||
test "it works for incoming rejects which are referenced by IRI only" do
|
||||
follower = insert(:actor)
|
||||
followed = insert(:actor)
|
||||
|
||||
# test "it fails for incoming rejects which cannot be correlated" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user, %{info: %{"locked" => true}})
|
||||
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
||||
|
||||
# accept_data =
|
||||
# File.read!("test/fixtures/mastodon-reject-activity.json")
|
||||
# |> Jason.decode!()
|
||||
# |> Map.put("actor", followed.ap_id)
|
||||
assert Actor.following?(follower, followed)
|
||||
|
||||
# accept_data =
|
||||
# Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
|
||||
reject_data =
|
||||
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
|
||||
# end
|
||||
test "it rejects activities without a valid ID" do
|
||||
actor = insert(:actor)
|
||||
|
||||
# test "it works for incoming rejects which are orphaned" do
|
||||
# follower = insert(:user)
|
||||
# followed = insert(:user, %{info: %{"locked" => true}})
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-follow-activity.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", actor.url)
|
||||
|> Map.put("id", "")
|
||||
|
||||
# {: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)
|
||||
|
||||
# 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
|
||||
:error = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it accepts Flag activities" do
|
||||
%Actor{url: reporter_url} = _reporter = insert(:actor)
|
||||
|
@ -279,6 +279,28 @@ defmodule MobilizonWeb.ActivityPubControllerTest do
|
||||
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
|
||||
# test "it returns the following in a collection", %{conn: conn} do
|
||||
|
@ -39,6 +39,7 @@ defmodule Mobilizon.Factory do
|
||||
url: Actor.build_url(preferred_username, :page),
|
||||
followers_url: Actor.build_url(preferred_username, :followers),
|
||||
following_url: Actor.build_url(preferred_username, :following),
|
||||
inbox_url: Actor.build_url(preferred_username, :inbox),
|
||||
outbox_url: Actor.build_url(preferred_username, :outbox),
|
||||
user: nil
|
||||
}
|
||||
@ -54,9 +55,13 @@ defmodule Mobilizon.Factory do
|
||||
end
|
||||
|
||||
def follower_factory do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
%Mobilizon.Actors.Follower{
|
||||
target_actor: build(:actor),
|
||||
actor: build(:actor)
|
||||
actor: build(:actor),
|
||||
id: uuid,
|
||||
url: "#{MobilizonWeb.Endpoint.url()}/follows/#{uuid}"
|
||||
}
|
||||
end
|
||||
|
||||
@ -118,6 +123,7 @@ defmodule Mobilizon.Factory do
|
||||
visibility: :public,
|
||||
tags: build_list(3, :tag),
|
||||
url: Routes.page_url(Endpoint, :event, uuid),
|
||||
picture: insert(:picture),
|
||||
uuid: uuid
|
||||
}
|
||||
end
|
||||
|
47
test/tasks/relay_test.exs
Normal file
47
test/tasks/relay_test.exs
Normal 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
|
Loading…
Reference in New Issue
Block a user