Refactor Core things, including Ecto handling, ActivityPub & Transmogrifier modules

* Data doesn't need anymore to be converted to ActivityStream format to
be saved (this was taken from Pleroma and not at all a good idea here)
* Everything saved when creating an event is inserted into PostgreSQL in
a single transaction
This commit is contained in:
Thomas Citharel 2019-10-25 17:43:37 +02:00
parent 68ba44113b
commit b3b41ad777
69 changed files with 1881 additions and 1424 deletions

View File

@ -14,7 +14,7 @@ export default {
data() { data() {
return { return {
participation: { participation: {
event: { event: {
title: 'Vue Styleguidist first meetup: learn the basics!', title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5, id: 5,
uuid: 'some uuid', uuid: 'some uuid',
@ -31,7 +31,7 @@ export default {
}, },
participantStats: { participantStats: {
approved: 1, approved: 1,
unapproved: 2 notApproved: 2
} }
}, },
actor: { actor: {
@ -75,20 +75,20 @@ export default {
</span> </span>
<span class="column is-narrow participant-stats"> <span class="column is-narrow participant-stats">
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0"> <span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.participants, total: participation.event.options.maximumAttendeeCapacity }) }} {{ $t('{approved} / {total} seats', {approved: participation.event.participantStats.participant, total: participation.event.options.maximumAttendeeCapacity }) }}
<!-- <b-progress--> <!-- <b-progress-->
<!-- v-if="participation.event.options.maximumAttendeeCapacity > 0"--> <!-- v-if="participation.event.options.maximumAttendeeCapacity > 0"-->
<!-- size="is-medium"--> <!-- size="is-medium"-->
<!-- :value="participation.event.participantStats.participants * 100 / participation.event.options.maximumAttendeeCapacity">--> <!-- :value="participation.event.participantStats.participant * 100 / participation.event.options.maximumAttendeeCapacity">-->
<!-- </b-progress>--> <!-- </b-progress>-->
</span> </span>
<span v-else> <span v-else>
{{ $tc('{count} participants', participation.event.participantStats.participants, { count: participation.event.participantStats.participants })}} {{ $tc('{count} participants', participation.event.participantStats.participant, { count: participation.event.participantStats.participant })}}
</span> </span>
<span <span
v-if="participation.event.participantStats.unapproved > 0"> v-if="participation.event.participantStats.notApproved > 0">
<b-button type="is-text" @click="gotToWithCheck(participation, { name: RouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } })"> <b-button type="is-text" @click="gotToWithCheck(participation, { name: RouteName.PARTICIPATIONS, params: { eventId: participation.event.uuid } })">
{{ $tc('{count} requests waiting', participation.event.participantStats.unapproved, { count: participation.event.participantStats.unapproved })}} {{ $tc('{count} requests waiting', participation.event.participantStats.notApproved, { count: participation.event.participantStats.notApproved })}}
</b-button> </b-button>
</span> </span>
</span> </span>

View File

@ -113,9 +113,8 @@ query LoggedUserParticipations($afterDateTime: DateTime, $beforeDateTime: DateTi
} }
}, },
participantStats { participantStats {
approved, notApproved
unapproved, participant
participants
}, },
options { options {
maximumAttendeeCapacity maximumAttendeeCapacity
@ -161,8 +160,8 @@ export const LOGGED_USER_DRAFTS = gql`
} }
}, },
participantStats { participantStats {
approved, going,
unapproved notApproved
}, },
options { options {
maximumAttendeeCapacity maximumAttendeeCapacity

View File

@ -102,9 +102,9 @@ export const FETCH_EVENT = gql`
# name, # name,
# }, # },
participantStats { participantStats {
approved, going,
unapproved, notApproved,
participants participant
}, },
tags { tags {
${tagsQuery} ${tagsQuery}
@ -259,9 +259,9 @@ export const CREATE_EVENT = gql`
id, id,
}, },
participantStats { participantStats {
approved, going,
unapproved, notApproved,
participants participant
}, },
tags { tags {
${tagsQuery} ${tagsQuery}
@ -344,9 +344,9 @@ export const EDIT_EVENT = gql`
id, id,
}, },
participantStats { participantStats {
approved, going,
unapproved, notApproved,
participants participant
}, },
tags { tags {
${tagsQuery} ${tagsQuery}
@ -410,10 +410,10 @@ export const PARTICIPANTS = gql`
${participantQuery} ${participantQuery}
}, },
participantStats { participantStats {
approved, going,
unapproved, notApproved,
rejected, rejected,
participants participant
} }
} }
} }

View File

@ -8,10 +8,10 @@ import { IPerson } from '@/types/actor';
@Component @Component
export default class EventMixin extends mixins(Vue) { export default class EventMixin extends mixins(Vue) {
async openDeleteEventModal (event: IEvent, currentActor: IPerson) { async openDeleteEventModal (event: IEvent, currentActor: IPerson) {
const participantsLength = event.participantStats.approved; const participantsLength = event.participantStats.participant;
const prefix = participantsLength const prefix = participantsLength
? this.$tc('There are {participants} participants.', event.participantStats.approved, { ? this.$tc('There are {participants} participants.', event.participantStats.participant, {
participants: event.participantStats.approved, participants: event.participantStats.participant,
}) })
: ''; : '';

View File

@ -94,10 +94,13 @@ export enum CommentModeration {
} }
export interface IEventParticipantStats { export interface IEventParticipantStats {
approved: number; notApproved: number;
unapproved: number;
rejected: number; rejected: number;
participants: number; participant: number;
creator: number;
moderator: number;
administrator: number;
going: number;
} }
export interface IEvent { export interface IEvent {
@ -192,7 +195,7 @@ export class EventModel implements IEvent {
publishAt = new Date(); publishAt = new Date();
participantStats = { approved: 0, unapproved: 0, rejected: 0, participants: 0 }; participantStats = { notApproved: 0, rejected: 0, participant: 0, moderator: 0, administrator: 0, creator: 0, going: 0 };
participants: IParticipant[] = []; participants: IParticipant[] = [];
relatedEvents: IEvent[] = []; relatedEvents: IEvent[] = [];

View File

@ -16,18 +16,18 @@ import {ParticipantRole} from "@/types/event.model";
<h1 class="title">{{ event.title }}</h1> <h1 class="title">{{ event.title }}</h1>
<span> <span>
<router-link v-if="actorIsOrganizer" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}"> <router-link v-if="actorIsOrganizer" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
<small v-if="event.participantStats.approved > 0 && !actorIsParticipant"> <small v-if="event.participantStats.going > 0 && !actorIsParticipant">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} {{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
</small> </small>
<small v-else-if="event.participantStats.approved > 0 && actorIsParticipant"> <small v-else-if="event.participantStats.going > 0 && actorIsParticipant">
{{ $tc('You and one other person are going to this event', event.participantStats.participants, { approved: event.participantStats.participants }) }} {{ $tc('You and one other person are going to this event', event.participantStats.participant, { approved: event.participantStats.participant }) }}
</small> </small>
</router-link> </router-link>
<small v-if="event.participantStats.approved > 0 && !actorIsParticipant && !actorIsOrganizer"> <small v-if="event.participantStats.going > 0 && !actorIsParticipant && !actorIsOrganizer">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }} {{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
</small> </small>
<small v-else-if="event.participantStats.approved > 0 && actorIsParticipant && !actorIsOrganizer"> <small v-else-if="event.participantStats.going > 0 && actorIsParticipant && !actorIsOrganizer">
{{ $tc('You and one other person are going to this event', event.participantStats.participants, { approved: event.participantStats.participants }) }} {{ $tc('You and one other person are going to this event', event.participantStats.participant, { approved: event.participantStats.participant }) }}
</small> </small>
<small v-if="event.options.maximumAttendeeCapacity"> <small v-if="event.options.maximumAttendeeCapacity">
{{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }} {{ $tc('All the places have already been taken', numberOfPlacesStillAvailable, { places: numberOfPlacesStillAvailable}) }}
@ -443,10 +443,10 @@ export default class Event extends EventMixin {
} }
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) { if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved + 1; event.participantStats.notApproved = event.participantStats.notApproved + 1;
} else { } else {
event.participantStats.approved = event.participantStats.approved + 1; event.participantStats.going = event.participantStats.going + 1;
event.participantStats.participants = event.participantStats.participants + 1; event.participantStats.participant = event.participantStats.participant + 1;
} }
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } }); store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
@ -514,10 +514,10 @@ export default class Event extends EventMixin {
return; return;
} }
if (participation.role === ParticipantRole.NOT_APPROVED) { if (participation.role === ParticipantRole.NOT_APPROVED) {
event.participantStats.unapproved = event.participantStats.unapproved - 1; event.participantStats.notApproved = event.participantStats.notApproved - 1;
} else { } else {
event.participantStats.approved = event.participantStats.approved - 1; event.participantStats.going = event.participantStats.going - 1;
event.participantStats.participants = event.participantStats.participants - 1; event.participantStats.participant = event.participantStats.participant - 1;
} }
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } }); store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.uuid }, data: { event } });
}, },
@ -591,11 +591,11 @@ export default class Event extends EventMixin {
get eventCapacityOK(): boolean { get eventCapacityOK(): boolean {
if (!this.event.options.maximumAttendeeCapacity) return true; if (!this.event.options.maximumAttendeeCapacity) return true;
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participants; return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
} }
get numberOfPlacesStillAvailable(): number { get numberOfPlacesStillAvailable(): number {
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participants; return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
} }
urlToHostname(url: string): string|null { urlToHostname(url: string): string|null {

View File

@ -4,7 +4,7 @@
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<b-icon icon="account-multiple"></b-icon> <b-icon icon="account-multiple"></b-icon>
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.approved }} </b-tag> </span> <span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
</template> </template>
<template> <template>
<section v-if="participantsAndCreators.length > 0"> <section v-if="participantsAndCreators.length > 0">
@ -22,10 +22,10 @@
</section> </section>
</template> </template>
</b-tab-item> </b-tab-item>
<b-tab-item :disabled="participantStats.unapproved === 0"> <b-tab-item :disabled="participantStats.notApproved === 0">
<template slot="header"> <template slot="header">
<b-icon icon="account-multiple-plus"></b-icon> <b-icon icon="account-multiple-plus"></b-icon>
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.unapproved }} </b-tag> </span> <span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
</template> </template>
<template> <template>
<section v-if="queue.length > 0"> <section v-if="queue.length > 0">
@ -182,7 +182,7 @@ export default class Participants extends Vue {
@Watch('participantStats', { deep: true }) @Watch('participantStats', { deep: true })
watchParticipantStats(stats: IEventParticipantStats) { watchParticipantStats(stats: IEventParticipantStats) {
if (!stats) return; if (!stats) return;
if ((stats.unapproved === 0 && this.activeTab === 1) || stats.rejected === 0 && this.activeTab === 2 ) { if ((stats.notApproved === 0 && this.activeTab === 1) || stats.rejected === 0 && this.activeTab === 2 ) {
this.activeTab = 0; this.activeTab = 0;
} }
} }
@ -223,9 +223,9 @@ export default class Participants extends Vue {
if (data) { if (data) {
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id); this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
this.rejected = this.rejected.filter(participant => participant.id !== data.updateParticipation.id); this.rejected = this.rejected.filter(participant => participant.id !== data.updateParticipation.id);
this.event.participantStats.approved += 1; this.event.participantStats.going += 1;
if (participant.role === ParticipantRole.NOT_APPROVED) { if (participant.role === ParticipantRole.NOT_APPROVED) {
this.event.participantStats.unapproved -= 1; this.event.participantStats.notApproved -= 1;
} }
if (participant.role === ParticipantRole.REJECTED) { if (participant.role === ParticipantRole.REJECTED) {
this.event.participantStats.rejected -= 1; this.event.participantStats.rejected -= 1;
@ -253,11 +253,11 @@ export default class Participants extends Vue {
this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id); this.queue = this.queue.filter(participant => participant.id !== data.updateParticipation.id);
this.event.participantStats.rejected += 1; this.event.participantStats.rejected += 1;
if (participant.role === ParticipantRole.PARTICIPANT) { if (participant.role === ParticipantRole.PARTICIPANT) {
this.event.participantStats.participants -= 1; this.event.participantStats.participant -= 1;
this.event.participantStats.approved -= 1; this.event.participantStats.going -= 1;
} }
if (participant.role === ParticipantRole.NOT_APPROVED) { if (participant.role === ParticipantRole.NOT_APPROVED) {
this.event.participantStats.unapproved -= 1; this.event.participantStats.notApproved -= 1;
} }
participant.role = ParticipantRole.REJECTED; participant.role = ParticipantRole.REJECTED;
this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id); this.rejected = this.rejected.filter(participantIn => participantIn.id !== participant.id);

View File

@ -0,0 +1,67 @@
defmodule Mix.Tasks.Mobilizon.MoveParticipantStats do
@moduledoc """
Temporary task to move participant stats in the events table
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Storage.Repo
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Events.ParticipantRole
import Ecto.Query
require Logger
@shortdoc "Move participant stats to events table"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts(
"\nStarting inserting participants stats into #{nb_events} events, this can take a while…\n"
)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([%Event{url: url} = event | events], nb_events) do
with roles <- ParticipantRole.__enum_map__(),
counts <-
Enum.reduce(roles, %{}, fn role, acc ->
Map.put(acc, role, count_participants(event, role))
end),
{:ok, _} <-
Events.update_event(event, %{
participant_stats: counts
}) do
Logger.debug("Added participants stats to event #{url}")
else
{:error, res} ->
Logger.error("Error while adding participants stats to event #{url} : #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([], nb_events) do
IO.puts("\nFinished inserting participant stats for #{nb_events} events!\n")
end
defp count_participants(%Event{id: event_id}, role) when is_atom(role) do
event_id
|> Events.count_participants_query()
|> Events.filter_role(role)
|> Repo.aggregate(:count, :id)
end
end

View File

@ -0,0 +1,49 @@
defmodule Mix.Tasks.Mobilizon.SetupSearch do
@moduledoc """
Temporary task to insert search data from existing events
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Service.Search
alias Mobilizon.Storage.Repo
alias Mobilizon.Events.Event
import Ecto.Query
require Logger
@shortdoc "Insert search data"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts("\nStarting setting up search for #{nb_events} events, this can take a while…\n")
insert_search_event(events, nb_events)
end
defp insert_search_event([%Event{url: url} = event | events], nb_events) do
case Search.insert_search_event(event) do
{:ok, _} ->
Logger.debug("Added event #{url} to the search")
{:error, res} ->
Logger.error("Error while adding event #{url} to the search: #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_search_event(events, nb_events)
end
defp insert_search_event([], nb_events) do
IO.puts("\nFinished setting up search for #{nb_events} events!\n")
end
end

View File

@ -5,18 +5,20 @@ defmodule Mix.Tasks.Mobilizon.Toot do
use Mix.Task use Mix.Task
alias MobilizonWeb.API alias MobilizonWeb.API.Comments
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
require Logger require Logger
@shortdoc "Toot to an user" @shortdoc "Toot to an user"
def run([from, content]) do def run([from, text]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")
case API.Comments.create_comment(from, content) do with {:local_actor, %Actor{} = actor} <- {:local_actor, Actors.get_local_actor_by_name(from)},
{:ok, _, _} -> {:ok, _, _} <- Comments.create_comment(%{actor: actor, text: text}) do
Mix.shell().info("Tooted") Mix.shell().info("Tooted")
else
{:local_actor, _, _} -> {:local_actor, _, _} ->
Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist") Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist")

View File

@ -13,6 +13,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
@ -46,6 +47,7 @@ defmodule Mobilizon.Actors.Actor do
created_reports: [Report.t()], created_reports: [Report.t()],
subject_reports: [Report.t()], subject_reports: [Report.t()],
report_notes: [Note.t()], report_notes: [Note.t()],
mentions: [Mention.t()],
memberships: [t] memberships: [t]
} }
@ -139,6 +141,7 @@ defmodule Mobilizon.Actors.Actor do
has_many(:created_reports, Report, foreign_key: :reporter_id) has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id) has_many(:report_notes, Note, foreign_key: :moderator_id)
has_many(:mentions, Mention)
many_to_many(:memberships, __MODULE__, join_through: Member) many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps() timestamps()

View File

@ -88,7 +88,10 @@ defmodule Mobilizon.Actors do
""" """
@spec get_actor_by_url(String.t(), boolean) :: @spec get_actor_by_url(String.t(), boolean) ::
{:ok, Actor.t()} | {:error, :actor_not_found} {:ok, Actor.t()} | {:error, :actor_not_found}
def get_actor_by_url(url, preload \\ false) do def get_actor_by_url(url, preload \\ false)
def get_actor_by_url(nil, _preload), do: {:error, :actor_not_found}
def get_actor_by_url(url, preload) do
case Repo.get_by(Actor, url: url) do case Repo.get_by(Actor, url: url) do
nil -> nil ->
{:error, :actor_not_found} {:error, :actor_not_found}

View File

@ -65,8 +65,7 @@ defmodule Mobilizon.Addresses.Address do
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
uuid = Ecto.UUID.generate() url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}")
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
put_change(changeset, :url, url) put_change(changeset, :url, url)
end end

View File

@ -8,7 +8,8 @@ defmodule Mobilizon.Events.Comment do
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, CommentVisibility, Event} alias Mobilizon.Events.{Comment, CommentVisibility, Event, Tag}
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
@ -22,6 +23,8 @@ defmodule Mobilizon.Events.Comment do
actor: Actor.t(), actor: Actor.t(),
attributed_to: Actor.t(), attributed_to: Actor.t(),
event: Event.t(), event: Event.t(),
tags: [Tag.t()],
mentions: [Mention.t()],
in_reply_to_comment: t, in_reply_to_comment: t,
origin_comment: t origin_comment: t
} }
@ -42,6 +45,8 @@ defmodule Mobilizon.Events.Comment do
belongs_to(:event, Event, foreign_key: :event_id) belongs_to(:event, Event, foreign_key: :event_id)
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id) belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id) belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
@ -57,16 +62,45 @@ defmodule Mobilizon.Events.Comment do
@doc false @doc false
@spec changeset(t, map) :: Ecto.Changeset.t() @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do def changeset(%__MODULE__{} = comment, attrs) do
uuid = attrs["uuid"] || Ecto.UUID.generate() uuid = Map.get(attrs, :uuid) || Ecto.UUID.generate()
url = attrs["url"] || generate_url(uuid) url = Map.get(attrs, :url) || generate_url(uuid)
comment comment
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> put_change(:uuid, uuid) |> put_change(:uuid, uuid)
|> put_change(:url, url) |> put_change(:url, url)
|> put_tags(attrs)
|> put_mentions(attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
end end
@spec generate_url(String.t()) :: String.t() @spec generate_url(String.t()) :: String.t()
defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid) defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid)
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}),
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(changeset, %{tags: tags}),
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(changeset, _), do: changeset
@spec put_mentions(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_mentions(changeset, %{"mentions" => mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, %{mentions: mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(tag) do
Tag.changeset(%Tag{}, tag)
end
defp process_mention(tag) do
Mention.changeset(%Mention{}, tag)
end
end end

View File

@ -6,22 +6,31 @@ defmodule Mobilizon.Events.Event do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Ecto.Changeset
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Addresses
alias Mobilizon.Events.{ alias Mobilizon.Events.{
EventOptions, EventOptions,
EventStatus, EventStatus,
EventVisibility, EventVisibility,
JoinOptions, JoinOptions,
EventParticipantStats,
Participant, Participant,
Session, Session,
Tag, Tag,
Track Track
} }
alias Mobilizon.Media
alias Mobilizon.Media.Picture alias Mobilizon.Media.Picture
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@type t :: %__MODULE__{ @type t :: %__MODULE__{
url: String.t(), url: String.t(),
@ -47,30 +56,15 @@ defmodule Mobilizon.Events.Event do
picture: Picture.t(), picture: Picture.t(),
tracks: [Track.t()], tracks: [Track.t()],
sessions: [Session.t()], sessions: [Session.t()],
mentions: [Mention.t()],
tags: [Tag.t()], tags: [Tag.t()],
participants: [Actor.t()] participants: [Actor.t()]
} }
@required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid] @update_required_attrs [:title, :begins_on, :organizer_actor_id]
@required_attrs @update_required_attrs ++ [:url, :uuid]
@optional_attrs [ @optional_attrs [
:slug,
:description,
:ends_on,
:category,
:status,
:draft,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
]
@attrs @required_attrs ++ @optional_attrs
@update_required_attrs @required_attrs
@update_optional_attrs [
:slug, :slug,
:description, :description,
:ends_on, :ends_on,
@ -85,7 +79,9 @@ defmodule Mobilizon.Events.Event do
:picture_id, :picture_id,
:physical_address_id :physical_address_id
] ]
@update_attrs @update_required_attrs ++ @update_optional_attrs @attrs @required_attrs ++ @optional_attrs
@update_attrs @update_required_attrs ++ @optional_attrs
schema "events" do schema "events" do
field(:url, :string) field(:url, :string)
@ -105,13 +101,15 @@ defmodule Mobilizon.Events.Event do
field(:phone_address, :string) field(:phone_address, :string)
field(:category, :string) field(:category, :string)
embeds_one(:options, EventOptions, on_replace: :update) embeds_one(:options, EventOptions, on_replace: :delete)
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id) belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
belongs_to(:physical_address, Address) belongs_to(:physical_address, Address, on_replace: :update)
belongs_to(:picture, Picture) belongs_to(:picture, Picture, on_replace: :update)
has_many(:tracks, Track) has_many(:tracks, Track)
has_many(:sessions, Session) has_many(:sessions, Session)
has_many(:mentions, Mention)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete) many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant) many_to_many(:participants, Actor, join_through: Participant)
@ -119,28 +117,41 @@ defmodule Mobilizon.Events.Event do
end end
@doc false @doc false
@spec changeset(t, map) :: Ecto.Changeset.t() @spec changeset(t, map) :: Changeset.t()
def changeset(%__MODULE__{} = event, attrs) do def changeset(%__MODULE__{} = event, attrs) do
attrs = Map.update(attrs, :uuid, Ecto.UUID.generate(), & &1)
attrs = Map.update(attrs, :url, Routes.page_url(Endpoint, :event, attrs.uuid), & &1)
event event
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:options) |> common_changeset(attrs)
|> put_creator_if_published(:create)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> validate_lengths() |> validate_lengths()
end end
@doc false @doc false
@spec update_changeset(t, map) :: Ecto.Changeset.t() @spec update_changeset(t, map) :: Changeset.t()
def update_changeset(%__MODULE__{} = event, attrs) do def update_changeset(%__MODULE__{} = event, attrs) do
event event
|> Ecto.Changeset.cast(attrs, @update_attrs) |> cast(attrs, @update_attrs)
|> cast_embed(:options) |> common_changeset(attrs)
|> put_tags(attrs) |> put_creator_if_published(:update)
|> validate_required(@update_required_attrs) |> validate_required(@update_required_attrs)
|> validate_lengths() |> validate_lengths()
end end
@spec validate_lengths(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec common_changeset(Changeset.t(), map) :: Changeset.t()
defp validate_lengths(%Ecto.Changeset{} = changeset) do defp common_changeset(%Changeset{} = changeset, attrs) do
changeset
|> cast_embed(:options)
|> put_tags(attrs)
|> put_address(attrs)
|> put_picture(attrs)
end
@spec validate_lengths(Changeset.t()) :: Changeset.t()
defp validate_lengths(%Changeset{} = changeset) do
changeset changeset
|> validate_length(:title, min: 3, max: 200) |> validate_length(:title, min: 3, max: 200)
|> validate_length(:online_address, min: 3, max: 2000) |> validate_length(:online_address, min: 3, max: 2000)
@ -161,7 +172,80 @@ defmodule Mobilizon.Events.Event do
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false} def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t() @spec put_tags(Changeset.t(), map) :: Changeset.t()
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags) defp put_tags(%Changeset{} = changeset, %{tags: tags}),
defp put_tags(changeset, _), do: changeset do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(%Changeset{} = changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(tag) do
Tag.changeset(%Tag{}, tag)
end
# In case the provided addresses is an existing one
@spec put_address(Changeset.t(), map) :: Changeset.t()
defp put_address(%Changeset{} = changeset, %{physical_address: %{id: id} = _physical_address}) do
case Addresses.get_address!(id) do
%Address{} = address ->
put_assoc(changeset, :physical_address, address)
_ ->
changeset
end
end
# In case it's a new address
defp put_address(%Changeset{} = changeset, _attrs) do
cast_assoc(changeset, :physical_address)
end
# In case the provided picture is an existing one
@spec put_picture(Changeset.t(), map) :: Changeset.t()
defp put_picture(%Changeset{} = changeset, %{picture: %{picture_id: id} = _picture}) do
case Media.get_picture!(id) do
%Picture{} = picture ->
put_assoc(changeset, :picture, picture)
_ ->
changeset
end
end
# In case it's a new picture
defp put_picture(%Changeset{} = changeset, _attrs) do
cast_assoc(changeset, :picture)
end
# Created or updated with draft parameter: don't publish
defp put_creator_if_published(
%Changeset{changes: %{draft: true}} = changeset,
_action
) do
cast_embed(changeset, :participant_stats)
end
# Created with any other value: publish
defp put_creator_if_published(
%Changeset{} = changeset,
:create
) do
changeset
|> put_embed(:participant_stats, %{creator: 1})
end
# Updated from draft false to true: publish
defp put_creator_if_published(
%Changeset{
data: %{draft: false},
changes: %{draft: true}
} = changeset,
:update
) do
changeset
|> put_embed(:participant_stats, %{creator: 1})
end
defp put_creator_if_published(%Changeset{} = changeset, _),
do: cast_embed(changeset, :participant_stats)
end end

View File

@ -0,0 +1,44 @@
defmodule Mobilizon.Events.EventParticipantStats do
@moduledoc """
Participation stats on event
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
not_approved: integer(),
rejected: integer(),
participant: integer(),
moderator: integer(),
administrator: integer(),
creator: integer()
}
@attrs [
:not_approved,
:rejected,
:participant,
:moderator,
:administrator,
:moderator,
:creator
]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:not_approved, :integer, default: 0)
field(:rejected, :integer, default: 0)
field(:participant, :integer, default: 0)
field(:moderator, :integer, default: 0)
field(:administrator, :integer, default: 0)
field(:creator, :integer, default: 0)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event_options, attrs) do
cast(event_options, attrs, @attrs)
end
end

View File

@ -7,6 +7,7 @@ defmodule Mobilizon.Events do
import Ecto.Query import Ecto.Query
import EctoEnum import EctoEnum
alias Ecto.{Multi, Changeset}
import Mobilizon.Storage.Ecto import Mobilizon.Storage.Ecto
@ -17,6 +18,7 @@ defmodule Mobilizon.Events do
alias Mobilizon.Events.{ alias Mobilizon.Events.{
Comment, Comment,
Event, Event,
EventParticipantStats,
FeedToken, FeedToken,
Participant, Participant,
Session, Session,
@ -82,6 +84,8 @@ defmodule Mobilizon.Events do
@event_preloads [ @event_preloads [
:organizer_actor, :organizer_actor,
:attributed_to,
:mentions,
:sessions, :sessions,
:tracks, :tracks,
:tags, :tags,
@ -90,7 +94,7 @@ defmodule Mobilizon.Events do
:picture :picture
] ]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment] @comment_preloads [:actor, :attributed_to, :in_reply_to_comment, :tags, :mentions]
@doc """ @doc """
Gets a single event. Gets a single event.
@ -235,81 +239,103 @@ defmodule Mobilizon.Events do
|> Repo.one() |> Repo.one()
end end
def get_or_create_event(%{"url" => url} = attrs) do
case Repo.get_by(Event, url: url) do
%Event{} = event -> {:ok, Repo.preload(event, @event_preloads)}
nil -> create_event(attrs)
end
end
@doc """ @doc """
Creates an event. Creates an event.
""" """
@spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} @spec create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def create_event(attrs \\ %{}) do def create_event(attrs \\ %{}) do
with {:ok, %Event{draft: false} = event} <- do_create_event(attrs), with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs),
{:ok, %Participant{} = _participant} <- %Event{} = event <- Repo.preload(event, @event_preloads) do
create_participant(%{
actor_id: event.organizer_actor_id,
role: :creator,
event_id: event.id
}) do
Task.start(fn -> Search.insert_search_event(event) end) Task.start(fn -> Search.insert_search_event(event) end)
{:ok, event} {:ok, event}
else else
# We don't create a creator participant if the event is a draft
{:ok, %Event{draft: true} = event} -> {:ok, event}
err -> err err -> err
end end
end end
@spec do_create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} # We start by inserting the event and then insert a first participant if the event is not a draft
@spec do_create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()}
defp do_create_event(attrs) do defp do_create_event(attrs) do
with {:ok, %Event{} = event} <- Multi.new()
%Event{} |> Multi.insert(:insert, Event.changeset(%Event{}, attrs))
|> Event.changeset(attrs) |> Multi.run(:write, fn _repo, %{insert: %Event{draft: draft} = event} ->
|> Ecto.Changeset.put_assoc(:tags, Map.get(attrs, "tags", [])) with {:is_draft, false} <- {:is_draft, draft},
|> Repo.insert(), {:ok, %Participant{} = participant} <-
%Event{} = event <- create_participant(
Repo.preload(event, [:tags, :organizer_actor, :physical_address, :picture]) do %{
{:ok, event} event_id: event.id,
end role: :creator,
actor_id: event.organizer_actor_id
},
false
) do
{:ok, participant}
else
{:is_draft, true} -> {:ok, nil}
err -> err
end
end)
|> Repo.transaction()
end end
@doc """ @doc """
Updates an event. Updates an event.
We start by updating the event and then insert a first participant if the event is not a draft anymore
""" """
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} @spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def update_event( def update_event(%Event{} = old_event, attrs) do
%Event{draft: old_draft_status, id: event_id, organizer_actor_id: organizer_actor_id} = with %Changeset{changes: changes} = changeset <-
old_event, Event.update_changeset(Repo.preload(old_event, :tags), attrs),
attrs {:ok, %{update: %Event{} = new_event}} <-
) do Multi.new()
with %Ecto.Changeset{changes: changes} = changeset <- |> Multi.update(
old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do :update,
with {:ok, %Event{draft: new_draft_status} = new_event} <- Repo.update(changeset) do changeset
# If the event is no longer a draft )
if old_draft_status == true && new_draft_status == false do |> Multi.run(:write, fn _repo, %{update: %Event{draft: draft} = event} ->
{:ok, %Participant{} = _participant} = with {:is_draft, false} <- {:is_draft, draft},
create_participant(%{ {:ok, %Participant{} = participant} <-
event_id: event_id, create_participant(
role: :creator, %{
actor_id: organizer_actor_id event_id: event.id,
}) role: :creator,
end actor_id: event.organizer_actor_id
},
false
) do
{:ok, participant}
else
{:is_draft, true} -> {:ok, nil}
err -> err
end
end)
|> Repo.transaction() do
Cachex.del(:ics, "event_#{new_event.uuid}")
Cachex.del(:ics, "event_#{new_event.uuid}") Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event,
new_event,
changes
)
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications( Task.start(fn -> Search.update_search_event(new_event) end)
old_event,
new_event,
changes
)
Task.start(fn -> Search.update_search_event(new_event) end) {:ok, Repo.preload(new_event, @event_preloads)}
{:ok, new_event}
end
end end
end end
@doc """ @doc """
Deletes an event. Deletes an event.
""" """
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()} @spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Changeset.t()}
def delete_event(%Event{} = event), do: Repo.delete(event) def delete_event(%Event{} = event), do: Repo.delete(event)
@doc """ @doc """
@ -450,7 +476,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Gets an existing tag or creates the new one. Gets an existing tag or creates the new one.
""" """
@spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} @spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(%{"name" => "#" <> title}) do def get_or_create_tag(%{"name" => "#" <> title}) do
case Repo.get_by(Tag, title: title) do case Repo.get_by(Tag, title: title) do
%Tag{} = tag -> %Tag{} = tag ->
@ -461,10 +487,24 @@ defmodule Mobilizon.Events do
end end
end end
@doc """
Gets an existing tag or creates the new one.
"""
@spec get_or_create_tag(String.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(title) do
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
{:ok, tag}
nil ->
create_tag(%{"title" => title})
end
end
@doc """ @doc """
Creates a tag. Creates a tag.
""" """
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} @spec create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def create_tag(attrs \\ %{}) do def create_tag(attrs \\ %{}) do
%Tag{} %Tag{}
|> Tag.changeset(attrs) |> Tag.changeset(attrs)
@ -474,7 +514,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Updates a tag. Updates a tag.
""" """
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} @spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def update_tag(%Tag{} = tag, attrs) do def update_tag(%Tag{} = tag, attrs) do
tag tag
|> Tag.changeset(attrs) |> Tag.changeset(attrs)
@ -484,7 +524,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Deletes a tag. Deletes a tag.
""" """
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()} @spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def delete_tag(%Tag{} = tag), do: Repo.delete(tag) def delete_tag(%Tag{} = tag), do: Repo.delete(tag)
@doc """ @doc """
@ -524,7 +564,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Creates a relation between two tags. Creates a relation between two tags.
""" """
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()} @spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Changeset.t()}
def create_tag_relation(attrs \\ {}) do def create_tag_relation(attrs \\ {}) do
%TagRelation{} %TagRelation{}
|> TagRelation.changeset(attrs) |> TagRelation.changeset(attrs)
@ -538,7 +578,7 @@ defmodule Mobilizon.Events do
Removes a tag relation. Removes a tag relation.
""" """
@spec delete_tag_relation(TagRelation.t()) :: @spec delete_tag_relation(TagRelation.t()) ::
{:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()} {:ok, TagRelation.t()} | {:error, Changeset.t()}
def delete_tag_relation(%TagRelation{} = tag_relation) do def delete_tag_relation(%TagRelation{} = tag_relation) do
Repo.delete(tag_relation) Repo.delete(tag_relation)
end end
@ -763,7 +803,7 @@ defmodule Mobilizon.Events do
end end
@doc """ @doc """
Counts participant participants. Counts participant participants (participants with no extra role)
""" """
@spec count_participant_participants(integer | String.t()) :: integer @spec count_participant_participants(integer | String.t()) :: integer
def count_participant_participants(event_id) do def count_participant_participants(event_id) do
@ -773,17 +813,6 @@ defmodule Mobilizon.Events do
|> Repo.aggregate(:count, :id) |> Repo.aggregate(:count, :id)
end end
@doc """
Counts unapproved participants.
"""
@spec count_unapproved_participants(integer | String.t()) :: integer
def count_unapproved_participants(event_id) do
event_id
|> count_participants_query()
|> filter_unapproved_role()
|> Repo.aggregate(:count, :id)
end
@doc """ @doc """
Counts rejected participants. Counts rejected participants.
""" """
@ -805,12 +834,40 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Creates a participant. Creates a participant.
""" """
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Ecto.Changeset.t()} @spec create_participant(map) :: {:ok, Participant.t()} | {:error, Changeset.t()}
def create_participant(attrs \\ %{}) do def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
with {:ok, %Participant{} = participant} <- with {:ok, %{participant: %Participant{} = participant}} <-
%Participant{} Multi.new()
|> Participant.changeset(attrs) |> Multi.insert(:participant, Participant.changeset(%Participant{}, attrs))
|> Repo.insert() do |> Multi.run(:update_event_participation_stats, fn _repo,
%{
participant:
%Participant{
role: role,
event_id: event_id
} = _participant
} ->
with {:update_event_participation_stats, true} <-
{:update_event_participation_stats, update_event_participation_stats},
{:ok, %Event{} = event} <- get_event(event_id),
%EventParticipantStats{} = participant_stats <-
Map.get(event, :participant_stats),
%EventParticipantStats{} = participant_stats <-
Map.update(participant_stats, role, 0, &(&1 + 1)),
{:ok, %Event{} = event} <-
event
|> Event.update_changeset(%{
participant_stats: Map.from_struct(participant_stats)
})
|> Repo.update() do
{:ok, event}
else
{:update_event_participation_stats, false} -> {:ok, nil}
{:error, :event_not_found} -> {:error, :event_not_found}
err -> {:error, err}
end
end)
|> Repo.transaction() do
{:ok, Repo.preload(participant, [:event, :actor])} {:ok, Repo.preload(participant, [:event, :actor])}
end end
end end
@ -819,7 +876,7 @@ defmodule Mobilizon.Events do
Updates a participant. Updates a participant.
""" """
@spec update_participant(Participant.t(), map) :: @spec update_participant(Participant.t(), map) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()} {:ok, Participant.t()} | {:error, Changeset.t()}
def update_participant(%Participant{} = participant, attrs) do def update_participant(%Participant{} = participant, attrs) do
participant participant
|> Participant.changeset(attrs) |> Participant.changeset(attrs)
@ -830,7 +887,7 @@ defmodule Mobilizon.Events do
Deletes a participant. Deletes a participant.
""" """
@spec delete_participant(Participant.t()) :: @spec delete_participant(Participant.t()) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()} {:ok, Participant.t()} | {:error, Changeset.t()}
def delete_participant(%Participant{} = participant), do: Repo.delete(participant) def delete_participant(%Participant{} = participant), do: Repo.delete(participant)
@doc """ @doc """
@ -843,7 +900,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Creates a session. Creates a session.
""" """
@spec create_session(map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} @spec create_session(map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def create_session(attrs \\ %{}) do def create_session(attrs \\ %{}) do
%Session{} %Session{}
|> Session.changeset(attrs) |> Session.changeset(attrs)
@ -853,7 +910,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Updates a session. Updates a session.
""" """
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} @spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def update_session(%Session{} = session, attrs) do def update_session(%Session{} = session, attrs) do
session session
|> Session.changeset(attrs) |> Session.changeset(attrs)
@ -863,7 +920,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Deletes a session. Deletes a session.
""" """
@spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()} @spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Changeset.t()}
def delete_session(%Session{} = session), do: Repo.delete(session) def delete_session(%Session{} = session), do: Repo.delete(session)
@doc """ @doc """
@ -892,7 +949,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Creates a track. Creates a track.
""" """
@spec create_track(map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} @spec create_track(map) :: {:ok, Track.t()} | {:error, Changeset.t()}
def create_track(attrs \\ %{}) do def create_track(attrs \\ %{}) do
%Track{} %Track{}
|> Track.changeset(attrs) |> Track.changeset(attrs)
@ -902,7 +959,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Updates a track. Updates a track.
""" """
@spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} @spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Changeset.t()}
def update_track(%Track{} = track, attrs) do def update_track(%Track{} = track, attrs) do
track track
|> Track.changeset(attrs) |> Track.changeset(attrs)
@ -912,7 +969,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Deletes a track. Deletes a track.
""" """
@spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()} @spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Changeset.t()}
def delete_track(%Track{} = track), do: Repo.delete(track) def delete_track(%Track{} = track), do: Repo.delete(track)
@doc """ @doc """
@ -931,6 +988,13 @@ defmodule Mobilizon.Events do
|> Repo.all() |> Repo.all()
end end
@doc """
Gets a single comment.
"""
@spec get_comment(integer | String.t()) :: Comment.t()
def get_comment(nil), do: nil
def get_comment(id), do: Repo.get(Comment, id)
@doc """ @doc """
Gets a single comment. Gets a single comment.
Raises `Ecto.NoResultsError` if the comment does not exist. Raises `Ecto.NoResultsError` if the comment does not exist.
@ -994,10 +1058,17 @@ defmodule Mobilizon.Events do
|> Repo.preload(@comment_preloads) |> Repo.preload(@comment_preloads)
end end
def get_or_create_comment(%{"url" => url} = attrs) do
case Repo.get_by(Comment, url: url) do
%Comment{} = comment -> {:ok, Repo.preload(comment, @comment_preloads)}
nil -> create_comment(attrs)
end
end
@doc """ @doc """
Creates a comment. Creates a comment.
""" """
@spec create_comment(map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} @spec create_comment(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def create_comment(attrs \\ %{}) do def create_comment(attrs \\ %{}) do
with {:ok, %Comment{} = comment} <- with {:ok, %Comment{} = comment} <-
%Comment{} %Comment{}
@ -1011,7 +1082,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Updates a comment. Updates a comment.
""" """
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} @spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def update_comment(%Comment{} = comment, attrs) do def update_comment(%Comment{} = comment, attrs) do
comment comment
|> Comment.changeset(attrs) |> Comment.changeset(attrs)
@ -1021,7 +1092,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Deletes a comment. Deletes a comment.
""" """
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()} @spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def delete_comment(%Comment{} = comment), do: Repo.delete(comment) def delete_comment(%Comment{} = comment), do: Repo.delete(comment)
@doc """ @doc """
@ -1097,7 +1168,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Creates a feed token. Creates a feed token.
""" """
@spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} @spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
def create_feed_token(attrs \\ %{}) do def create_feed_token(attrs \\ %{}) do
attrs = Map.put(attrs, "token", Ecto.UUID.generate()) attrs = Map.put(attrs, "token", Ecto.UUID.generate())
@ -1110,7 +1181,7 @@ defmodule Mobilizon.Events do
Updates a feed token. Updates a feed token.
""" """
@spec update_feed_token(FeedToken.t(), map) :: @spec update_feed_token(FeedToken.t(), map) ::
{:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} {:ok, FeedToken.t()} | {:error, Changeset.t()}
def update_feed_token(%FeedToken{} = feed_token, attrs) do def update_feed_token(%FeedToken{} = feed_token, attrs) do
feed_token feed_token
|> FeedToken.changeset(attrs) |> FeedToken.changeset(attrs)
@ -1120,7 +1191,7 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Deletes a feed token. Deletes a feed token.
""" """
@spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()} @spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
def delete_feed_token(%FeedToken{} = feed_token), do: Repo.delete(feed_token) def delete_feed_token(%FeedToken{} = feed_token), do: Repo.delete(feed_token)
@doc """ @doc """
@ -1354,7 +1425,7 @@ defmodule Mobilizon.Events do
end end
@spec count_participants_query(integer) :: Ecto.Query.t() @spec count_participants_query(integer) :: Ecto.Query.t()
defp count_participants_query(event_id) do def count_participants_query(event_id) do
from(p in Participant, where: p.event_id == ^event_id) from(p in Participant, where: p.event_id == ^event_id)
end end
@ -1385,17 +1456,10 @@ defmodule Mobilizon.Events do
end end
defp public_comments_for_actor_query(actor_id) do defp public_comments_for_actor_query(actor_id) do
from( Comment
c in Comment, |> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
where: c.actor_id == ^actor_id and c.visibility in ^@public_visibility, |> order_by([c], desc: :id)
order_by: [desc: :id], |> preload_for_comment()
preload: [
:actor,
:in_reply_to_comment,
:origin_comment,
:event
]
)
end end
@spec list_participants_for_event_query(String.t()) :: Ecto.Query.t() @spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
@ -1498,31 +1562,30 @@ defmodule Mobilizon.Events do
@spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t() @spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_approved_role(query) do defp filter_approved_role(query) do
from(p in query, where: p.role not in ^[:not_approved, :rejected]) filter_role(query, [:not_approved, :rejected])
end end
@spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t() @spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_participant_role(query) do defp filter_participant_role(query) do
from(p in query, where: p.role == ^:participant) filter_role(query, :participant)
end
@spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_unapproved_role(query) do
from(p in query, where: p.role == ^:not_approved)
end end
@spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t() @spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_rejected_role(query) do defp filter_rejected_role(query) do
from(p in query, where: p.role == ^:rejected) filter_role(query, :rejected)
end end
@spec filter_role(Ecto.Query.t(), list(atom())) :: Ecto.Query.t() @spec filter_role(Ecto.Query.t(), list(atom())) :: Ecto.Query.t()
defp filter_role(query, []), do: query def filter_role(query, []), do: query
defp filter_role(query, roles) do def filter_role(query, roles) when is_list(roles) do
where(query, [p], p.role in ^roles) where(query, [p], p.role in ^roles)
end end
def filter_role(query, role) when is_atom(role) do
from(p in query, where: p.role == ^role)
end
defp participation_filter_begins_on(query, nil, nil), defp participation_filter_begins_on(query, nil, nil),
do: participation_order_begins_on_desc(query) do: participation_order_begins_on_desc(query)

View File

@ -0,0 +1,53 @@
defmodule Mobilizon.Mention do
@moduledoc """
The Mentions context.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Storage.Repo
@type t :: %__MODULE__{
silent: boolean,
actor: Actor.t(),
event: Event.t(),
comment: Comment.t()
}
@required_attrs [:actor_id]
@optional_attrs [:silent, :event_id, :comment_id]
@attrs @required_attrs ++ @optional_attrs
schema "mentions" do
field(:silent, :boolean, default: false)
belongs_to(:actor, Actor)
belongs_to(:event, Event)
belongs_to(:comment, Comment)
timestamps()
end
@doc false
def changeset(event, attrs) do
event
|> cast(attrs, @attrs)
# TODO: Enforce having either event_id or comment_id
|> validate_required(@required_attrs)
end
@doc """
Creates a new mention
"""
@spec create_mention(map()) :: {:ok, t} | {:error, Ecto.Changeset.t()}
def create_mention(args) do
with {:ok, %__MODULE__{} = mention} <-
%__MODULE__{}
|> changeset(args)
|> Repo.insert() do
{:ok, Repo.preload(mention, [:actor, :event, :comment])}
end
end
end

View File

@ -2,55 +2,18 @@ defmodule MobilizonWeb.API.Comments do
@moduledoc """ @moduledoc """
API for Comments. API for Comments.
""" """
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Activity
alias MobilizonWeb.API.Utils
@doc """ @doc """
Create a comment Create a comment
Creates a comment from an actor and a status Creates a comment from an actor and a status
""" """
@spec create_comment(String.t(), String.t(), String.t()) :: @spec create_comment(map()) ::
{:ok, Activity.t(), Comment.t()} | any() {:ok, Activity.t(), Comment.t()} | any()
def create_comment( def create_comment(args) do
from_username, ActivityPub.create(:comment, args, true)
status,
visibility \\ :public,
in_reply_to_comment_URL \\ nil
) do
with {:local_actor, %Actor{url: url} = actor} <-
{:local_actor, Actors.get_local_actor_by_name(from_username)},
in_reply_to_comment <- get_in_reply_to_comment(in_reply_to_comment_URL),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, status, visibility, [], in_reply_to_comment),
comment <-
ActivityPubUtils.make_comment_data(
url,
to,
content_html,
in_reply_to_comment,
tags,
cc
) do
ActivityPub.create(%{
to: to,
actor: actor,
object: comment,
local: true
})
end
end
@spec get_in_reply_to_comment(nil) :: nil
defp get_in_reply_to_comment(nil), do: nil
@spec get_in_reply_to_comment(String.t()) :: Comment.t()
defp get_in_reply_to_comment(in_reply_to_comment_url) do
ActivityPub.fetch_object_from_url(in_reply_to_comment_url)
end end
end end

View File

@ -3,37 +3,24 @@ defmodule MobilizonWeb.API.Events do
API for Events. API for Events.
""" """
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Utils
alias MobilizonWeb.API.Utils
@doc """ @doc """
Create an event Create an event
""" """
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any() @spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
def create_event(%{organizer_actor: organizer_actor} = args) do def create_event(args) do
with args <- prepare_args(args), with organizer_actor <- Map.get(args, :organizer_actor),
event <- args <-
ActivityPubUtils.make_event_data( Map.update(args, :picture, nil, fn picture ->
args.organizer_actor.url, process_picture(picture, organizer_actor)
%{to: args.to, cc: args.cc}, end) do
args.title, # For now we don't federate drafts but it will be needed if we want to edit them as groups
args.content_html, ActivityPub.create(:event, args, args.draft == false)
args.picture,
args.tags,
args.metadata
) do
ActivityPub.create(%{
to: args.to,
actor: organizer_actor,
object: event,
# For now we don't federate drafts but it will be needed if we want to edit them as groups
local: args.metadata.draft == false
})
end end
end end
@ -41,65 +28,13 @@ defmodule MobilizonWeb.API.Events do
Update an event Update an event
""" """
@spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any() @spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
def update_event( def update_event(args, %Event{} = event) do
%{ with organizer_actor <- Map.get(args, :organizer_actor),
organizer_actor: organizer_actor args <-
} = args, Map.update(args, :picture, nil, fn picture ->
%Event{} = event process_picture(picture, organizer_actor)
) do end) do
with args <- Map.put(args, :tags, Map.get(args, :tags, [])), ActivityPub.update(:event, event, args, Map.get(args, :draft, false) == false)
args <- prepare_args(Map.merge(event, args)),
event <-
ActivityPubUtils.make_event_data(
args.organizer_actor.url,
%{to: args.to, cc: args.cc},
args.title,
args.content_html,
args.picture,
args.tags,
args.metadata,
event.uuid,
event.url
) do
ActivityPub.update(%{
to: args.to,
actor: organizer_actor.url,
cc: [],
object: event,
local: args.metadata.draft == false
})
end
end
defp prepare_args(args) do
with %Actor{} = organizer_actor <- Map.get(args, :organizer_actor),
title <- args |> Map.get(:title, "") |> HtmlSanitizeEx.strip_tags() |> String.trim(),
visibility <- Map.get(args, :visibility, :public),
description <- Map.get(args, :description),
tags <- Map.get(args, :tags),
{content_html, tags, to, cc} <-
Utils.prepare_content(organizer_actor, description, visibility, tags, nil) do
%{
title: title,
content_html: content_html,
picture: Map.get(args, :picture),
tags: tags,
organizer_actor: organizer_actor,
to: to,
cc: cc,
metadata: %{
begins_on: Map.get(args, :begins_on),
ends_on: Map.get(args, :ends_on),
physical_address: Map.get(args, :physical_address),
category: Map.get(args, :category),
options: Map.get(args, :options),
join_options: Map.get(args, :join_options),
status: Map.get(args, :status),
online_address: Map.get(args, :online_address),
phone_address: Map.get(args, :phone_address),
draft: Map.get(args, :draft)
}
}
end end
end end
@ -111,4 +46,15 @@ defmodule MobilizonWeb.API.Events do
def delete_event(%Event{} = event, federate \\ true) do def delete_event(%Event{} = event, federate \\ true) do
ActivityPub.delete(event, federate) ActivityPub.delete(event, federate)
end end
defp process_picture(nil, _), do: nil
defp process_picture(%{picture_id: _picture_id} = args, _), do: args
defp process_picture(%{picture: picture}, %Actor{id: actor_id}) do
%{
file:
picture |> Map.get(:file) |> Utils.make_picture_data(description: Map.get(picture, :name)),
actor_id: actor_id
}
end
end end

View File

@ -6,6 +6,7 @@ defmodule MobilizonWeb.API.Follows do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
require Logger require Logger
@ -32,17 +33,14 @@ defmodule MobilizonWeb.API.Follows do
end end
def accept(%Actor{} = follower, %Actor{} = followed) do def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <- with %Follower{approved: false} = follow <-
Actors.is_following(follower, followed), Actors.is_following(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}", {:ok, %Activity{} = activity, %Follower{approved: true}} <-
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
{:ok, activity, _} <-
ActivityPub.accept( ActivityPub.accept(
%{to: [follower.url], actor: followed.url, object: data}, :follow,
activity_follow_url follow,
), %{approved: true}
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do ) do
{:ok, activity} {:ok, activity}
else else
%Follower{approved: true} -> %Follower{approved: true} ->

View File

@ -6,38 +6,19 @@ defmodule MobilizonWeb.API.Groups do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
alias MobilizonWeb.API.Utils
@doc """ @doc """
Create a group Create a group
""" """
@spec create_group(User.t(), map()) :: {:ok, Activity.t(), Group.t()} | any() @spec create_group(map()) :: {:ok, Activity.t(), Actor.t()} | any()
def create_group( def create_group(args) do
user, with preferred_username <-
%{ args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
preferred_username: title, {:existing_group, nil} <-
summary: summary, {:existing_group, Actors.get_local_group_by_title(preferred_username)},
creator_actor_id: creator_actor_id, {:ok, %Activity{} = activity, %Actor{} = group} <- ActivityPub.create(:group, args, true) do
avatar: _avatar, {:ok, activity, group}
banner: _banner
} = args
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
title <- String.trim(title),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
visibility <- Map.get(args, :visibility, :public),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, summary, visibility, [], nil),
group <- ActivityPubUtils.make_group_data(actor.url, to, title, content_html, tags, cc) do
ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: actor,
object: group,
local: true
})
else else
{:existing_group, _} -> {:existing_group, _} ->
{:error, "A group with this name already exists"} {:error, "A group with this name already exists"}

View File

@ -38,12 +38,11 @@ defmodule MobilizonWeb.API.Participations do
) do ) do
with {:ok, activity, _} <- with {:ok, activity, _} <-
ActivityPub.accept( ActivityPub.accept(
%{ :join,
to: [participation.actor.url], participation,
actor: moderator.url, %{role: :participant},
object: participation.url true,
}, %{"to" => [moderator.url]}
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participation.id}"
), ),
{:ok, %Participant{role: :participant} = participation} <- {:ok, %Participant{role: :participant} = participation} <-
Events.update_participant(participation, %{"role" => :participant}), Events.update_participant(participation, %{"role" => :participant}),

View File

@ -3,89 +3,9 @@ defmodule MobilizonWeb.API.Utils do
Utils for API. Utils for API.
""" """
alias Mobilizon.Actors.Actor
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Service.Formatter alias Mobilizon.Service.Formatter
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@doc """
Determines the full audience based on mentions for a public audience
Audience is:
* `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()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :public) do
to = [@ap_public | mentions]
cc = [actor.followers_url]
if inReplyTo do
{Enum.uniq([inReplyTo.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a unlisted audience
Audience is:
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
* `cc` : public
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :unlisted) do
to = [actor.followers_url | mentions]
cc = [@ap_public]
if inReplyTo do
{Enum.uniq([inReplyTo.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a private audience
Audience is:
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :private) do
{to, cc} = get_to_and_cc(actor, mentions, inReplyTo, :direct)
{[actor.followers_url | to], cc}
end
@doc """
Determines the full audience based on mentions based on a direct audience
Audience is:
* `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, inReplyTo, :direct) do
if inReplyTo do
{Enum.uniq([inReplyTo.actor | mentions]), []}
else
{mentions, []}
end
end
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)
# end
def get_addressed_users(mentioned_users, _), do: mentioned_users
@doc """ @doc """
Creates HTML content from text and mentions Creates HTML content from text and mentions
""" """
@ -126,19 +46,4 @@ defmodule MobilizonWeb.API.Utils do
{:error, "Comment must be up to #{max_size} characters"} {:error, "Comment must be up to #{max_size} characters"}
end end
end end
def prepare_content(actor, content, visibility, tags, in_reply_to) do
with content <- String.trim(content || ""),
{content_html, mentions, tags} <-
make_content_html(
content,
tags,
"text/html"
),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
addressed_users <- get_addressed_users(mentioned_users, nil),
{to, cc} <- get_to_and_cc(actor, addressed_users, in_reply_to, visibility) do
{content_html, tags, to, cc}
end
end
end end

View File

@ -4,19 +4,19 @@ defmodule MobilizonWeb.Resolvers.Comment do
""" """
alias Mobilizon.Events.Comment alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Actors.Actor
alias MobilizonWeb.API.Comments alias MobilizonWeb.API.Comments
require Logger require Logger
def create_comment(_parent, %{text: comment, actor_username: username}, %{ def create_comment(_parent, %{text: text, actor_id: actor_id}, %{
context: %{current_user: %User{} = _user} context: %{current_user: %User{} = user}
}) do }) do
with {:ok, %Activity{data: %{"object" => %{"type" => "Note"} = _object}}, with {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
%Comment{} = comment} <- {:ok, _, %Comment{} = comment} <-
Comments.create_comment(username, comment) do Comments.create_comment(%{actor_id: actor_id, text: text}) do
{:ok, comment} {:ok, comment}
end end
end end

View File

@ -7,11 +7,8 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant, EventParticipantStats}
alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub.Activity alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -96,14 +93,8 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok, []} {:ok, []}
end end
def stats_participants_for_event(%Event{id: id}, _args, _resolution) do def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
{:ok, {:ok, stats.participant + stats.moderator + stats.administrator + stats.creator}
%{
approved: Mobilizon.Events.count_approved_participants(id),
unapproved: Mobilizon.Events.count_unapproved_participants(id),
rejected: Mobilizon.Events.count_rejected_participants(id),
participants: Mobilizon.Events.count_participant_participants(id)
}}
end end
@doc """ @doc """
@ -277,8 +268,6 @@ defmodule MobilizonWeb.Resolvers.Event do
with args <- Map.put(args, :options, args[:options] || %{}), with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id), {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor), args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args_with_organizer} <- save_attached_picture(args_with_organizer),
{:ok, args_with_organizer} <- save_physical_address(args_with_organizer),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <- {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event} {:ok, event}
@ -309,8 +298,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:is_owned, %Actor{} = organizer_actor} <- {:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, event.organizer_actor_id), User.owns_actor(user, event.organizer_actor_id),
args <- Map.put(args, :organizer_actor, organizer_actor), args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <- {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.update_event(args, event) do MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event} {:ok, event}
@ -327,47 +314,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to update an event"} {:error, "You need to be logged-in to update an event"}
end end
# If we have an attached picture, just transmit it. It will be handled by
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
# However, we need to pass its actor ID
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor.id))}
end
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)}
end
end
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(args), do: {:ok, args}
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
when not is_nil(physical_address_url) do
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
args <- Map.put(args, :physical_address, address.url) do
{:ok, args}
end
end
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: address} = args) when address != nil do
with {:ok, %Address{} = address} <- Addresses.create_address(address),
args <- Map.put(args, :physical_address, address.url) do
{:ok, args}
end
end
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(args), do: {:ok, args}
@doc """ @doc """
Delete an event Delete an event
""" """

View File

@ -6,7 +6,6 @@ defmodule MobilizonWeb.Resolvers.Group do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias MobilizonWeb.API alias MobilizonWeb.API
@ -47,23 +46,18 @@ defmodule MobilizonWeb.Resolvers.Group do
args, args,
%{context: %{current_user: user}} %{context: %{current_user: user}}
) do ) do
with { with creator_actor_id <- Map.get(args, :creator_actor_id),
:ok, {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
%Activity{data: %{"object" => %{"type" => "Group"} = _object}}, args <- Map.put(args, :creator_actor, actor),
%Actor{} = group {:ok, _activity, %Actor{type: :Group} = group} <-
} <- API.Groups.create_group(args) do
API.Groups.create_group(
user,
%{
preferred_username: args.preferred_username,
creator_actor_id: args.creator_actor_id,
name: Map.get(args, "name", args.preferred_username),
summary: args.summary,
avatar: Map.get(args, "avatar"),
banner: Map.get(args, "banner")
}
) do
{:ok, group} {:ok, group}
else
{:error, err} when is_bitstring(err) ->
{:error, err}
{:is_owned, nil} ->
{:error, "Creator actor id is not owned by the current user"}
end end
end end

View File

@ -155,7 +155,7 @@ defmodule MobilizonWeb.Resolvers.Person do
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
pic = args[key][:picture] pic = args[key][:picture]
with {:ok, %{"name" => name, "url" => [%{"href" => url, "mediaType" => content_type}]}} <- with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type}) Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end end

View File

@ -51,7 +51,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
%{context: %{current_user: user}} %{context: %{current_user: user}}
) do ) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id), with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <- {:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
MobilizonWeb.Upload.store(file), MobilizonWeb.Upload.store(file),
args <- args <-
args args

View File

@ -35,7 +35,7 @@ defmodule MobilizonWeb.Schema.CommentType do
@desc "Create a comment" @desc "Create a comment"
field :create_comment, type: :comment do field :create_comment, type: :comment do
arg(:text, non_null(:string)) arg(:text, non_null(:string))
arg(:actor_username, non_null(:string)) arg(:actor_id, non_null(:id))
resolve(&Comment.create_comment/3) resolve(&Comment.create_comment/3)
end end

View File

@ -63,7 +63,7 @@ defmodule MobilizonWeb.Schema.EventType do
field(:draft, :boolean, description: "Whether or not the event is a draft") field(:draft, :boolean, description: "Whether or not the event is a draft")
field(:participant_stats, :participant_stats, resolve: &Event.stats_participants_for_event/3) field(:participant_stats, :participant_stats)
field(:participants, list_of(:participant), description: "The event's participants") do field(:participants, list_of(:participant), description: "The event's participants") do
arg(:page, :integer, default_value: 1) arg(:page, :integer, default_value: 1)
@ -112,13 +112,21 @@ defmodule MobilizonWeb.Schema.EventType do
end end
object :participant_stats do object :participant_stats do
field(:approved, :integer, description: "The number of approved participants") field(:going, :integer,
field(:unapproved, :integer, description: "The number of unapproved participants") description: "The number of approved participants",
resolve: &Event.stats_participants_going/3
)
field(:not_approved, :integer, description: "The number of not approved participants")
field(:rejected, :integer, description: "The number of rejected participants") field(:rejected, :integer, description: "The number of rejected participants")
field(:participants, :integer, field(:participant, :integer,
description: "The number of simple participants (excluding creators)" description: "The number of simple participants (excluding creators)"
) )
field(:moderator, :integer, description: "The number of moderators")
field(:administrator, :integer, description: "The number of administrators")
field(:creator, :integer, description: "The number of creators")
end end
object :event_offer do object :event_offer do

View File

@ -73,16 +73,10 @@ defmodule MobilizonWeb.Upload do
{:ok, url_spec} <- Uploaders.Uploader.put_file(opts.uploader, upload) do {:ok, url_spec} <- Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok, {:ok,
%{ %{
"type" => opts.activity_type || get_type(upload.content_type), name: Map.get(opts, :description) || upload.name,
"url" => [ url: url_from_spec(upload, opts.base_url, url_spec),
%{ content_type: upload.content_type,
"type" => "Link", size: upload.size
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"size" => upload.size,
"name" => Map.get(opts, :description) || upload.name
}} }}
else else
{:error, error} -> {:error, error} ->
@ -166,16 +160,6 @@ defmodule MobilizonWeb.Upload do
defp check_file_size(_, _), do: :ok defp check_file_size(_, _), do: :ok
@picture_content_types ["image/gif", "image/png", "image/jpg", "image/jpeg", "image/webp"]
# Return whether the upload is a picture or not
defp get_type(content_type) do
if content_type in @picture_content_types do
"Image"
else
"Document"
end
end
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path = path =
URI.encode(path, &char_unescaped?/1) <> URI.encode(path, &char_unescaped?/1) <>

View File

@ -8,7 +8,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ActivityPub context. # ActivityPub context.
""" """
import Mobilizon.Service.ActivityPub.{Utils, Visibility} import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
alias Mobilizon.{Actors, Config, Events} alias Mobilizon.{Actors, Config, Events}
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
@ -16,17 +17,12 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Relay, Transmogrifier} alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Relay, Transmogrifier}
alias Mobilizon.Service.{Federator, WebFinger} alias Mobilizon.Service.{Federator, WebFinger}
alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.HTTPSignatures.Signature
alias MobilizonWeb.API.Utils, as: APIUtils
alias Mobilizon.Service.ActivityPub.Audience
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger require Logger
@doc """
Get recipients for an activity or object
"""
@spec get_recipients(map()) :: list()
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
@doc """ @doc """
Wraps an object into an activity Wraps an object into an activity
""" """
@ -106,8 +102,8 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """ @doc """
Getting an actor from url, eventually creating it Getting an actor from url, eventually creating it
""" """
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()} @spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do def get_or_fetch_actor_by_url(url, preload \\ false) do
case Actors.get_actor_by_url(url, preload) do case Actors.get_actor_by_url(url, preload) do
{:ok, %Actor{} = actor} -> {:ok, %Actor{} = actor} ->
{:ok, actor} {:ok, actor}
@ -126,27 +122,29 @@ defmodule Mobilizon.Service.ActivityPub do
end end
@doc """ @doc """
Create an activity of type "Create" Create an activity of type `Create`
"""
def create(%{to: to, actor: actor, object: object} = params) do
Logger.debug("creating an activity")
Logger.debug(inspect(params))
Logger.debug(inspect(object))
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with create_data <- * Creates the object, which returns AS data
make_create_data( * Wraps ActivityStreams data into a `Create` activity
%{to: to, actor: actor, published: published, object: object}, * Creates an `Mobilizon.Service.ActivityPub.Activity` from this
additional * Federates (asynchronously) the activity
), * Returns the activity
{:ok, activity} <- create_activity(create_data, local), """
{:ok, object} <- insert_full_object(create_data), @spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
def create(type, args, local \\ false, additional \\ %{}) do
Logger.debug("creating an activity")
Logger.debug(inspect(args))
{:ok, entity, create_data} =
case type do
:event -> create_event(args, additional)
:comment -> create_comment(args, additional)
:group -> create_group(args, additional)
end
with {:ok, activity} <- create_activity(create_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do {:ok, activity, entity}
{:ok, activity, object}
else else
err -> err ->
Logger.error("Something went wrong while creating an activity") Logger.error("Something went wrong while creating an activity")
@ -155,21 +153,52 @@ defmodule Mobilizon.Service.ActivityPub do
end end
end end
def accept(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do @doc """
# only accept false as false value Create an activity of type `Update`
local = !(params[:local] == false)
with data <- %{ * Updates the object, which returns AS data
"to" => to, * Wraps ActivityStreams data into a `Update` activity
"type" => "Accept", * Creates an `Mobilizon.Service.ActivityPub.Activity` from this
"actor" => actor, * Federates (asynchronously) the activity
"object" => object, * Returns the activity
"id" => activity_wrapper_id || get_url(object) <> "/activity" """
}, @spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
{:ok, activity} <- create_activity(data, local), def update(type, old_entity, args, local \\ false, additional \\ %{}) do
{:ok, object} <- insert_full_object(data), Logger.debug("updating an activity")
Logger.debug(inspect(args))
{:ok, entity, update_data} =
case type do
:event -> update_event(old_entity, args, additional)
:actor -> update_actor(old_entity, args, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
def accept(type, entity, args, local \\ false, additional \\ %{}) do
{:ok, entity, update_data} =
case type do
:join -> accept_join(entity, args, additional)
:follow -> accept_follow(entity, args, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end end
end end
@ -191,25 +220,6 @@ defmodule Mobilizon.Service.ActivityPub do
end end
end end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"id" => object["url"],
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- update_object(object["id"], data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
# def like( # def like(
# %Actor{url: url} = actor, # %Actor{url: url} = actor,
@ -290,15 +300,12 @@ defmodule Mobilizon.Service.ActivityPub do
Make an actor follow another Make an actor follow another
""" """
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <- with {:ok, %Follower{} = follower} <-
Actors.follow(followed, follower, activity_id, false), Actors.follow(followed, follower, activity_id, false),
activity_follow_id <- follower_as_data <- Convertible.model_to_as(follower),
activity_id || follow_url, {:ok, activity} <- create_activity(follower_as_data, local),
data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, follower}
else else
{:error, err, msg} when err in [:already_following, :suspended] -> {:error, err, msg} when err in [:already_following, :suspended] ->
{:error, msg} {:error, msg}
@ -310,16 +317,11 @@ defmodule Mobilizon.Service.ActivityPub do
""" """
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower), with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
# We recreate the follow activity # We recreate the follow activity
data <- follow_as_data <-
make_follow_data( Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
followed, {:ok, follow_activity} <- create_activity(follow_as_data, local),
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity} <- create_activity(data, local),
{:ok, _object} <- insert_full_object(data),
activity_unfollow_id <- activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity", activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <- unfollow_data <-
@ -346,7 +348,7 @@ defmodule Mobilizon.Service.ActivityPub do
"id" => url <> "/delete" "id" => url <> "/delete"
} }
with {:ok, _} <- Events.delete_event(event), with {:ok, %Event{} = event} <- Events.delete_event(event),
{:ok, activity} <- create_activity(data, local), {:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, event} {:ok, activity, event}
@ -362,11 +364,10 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
} }
with {:ok, _} <- Events.delete_comment(comment), with {:ok, %Comment{} = comment} <- Events.delete_comment(comment),
{:ok, activity} <- create_activity(data, local), {:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, comment}
end end
end end
@ -379,11 +380,10 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] "to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
} }
with {:ok, _} <- Actors.delete_actor(actor), with {:ok, %Actor{} = actor} <- Actors.delete_actor(actor),
{:ok, activity} <- create_activity(data, local), {:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, actor}
end end
end end
@ -434,9 +434,9 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _object} <- insert_full_object(join_data), {:ok, _object} <- insert_full_object(join_data),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
if role === :participant do if role === :participant do
accept( accept_join(
%{to: [actor.url], actor: event.organizer_actor.url, object: join_data["id"]}, participant,
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}" %{}
) )
end end
@ -720,9 +720,233 @@ defmodule Mobilizon.Service.ActivityPub do
} }
end end
# # Whether the Public audience is in the activity's audience # Get recipients for an activity or object
# defp is_public?(activity) do @spec get_recipients(map()) :: list()
# "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ defp get_recipients(data) do
# (activity.data["cc"] || [])) (data["to"] || []) ++ (data["cc"] || [])
# end end
@spec create_event(map(), map()) :: {:ok, map()}
defp create_event(args, additional) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = event} <- Events.create_event(args),
event_as_data <- Convertible.model_to_as(event),
audience <-
Audience.calculate_to_and_cc_from_mentions(
event.organizer_actor,
args.mentions,
nil,
event.visibility
),
create_data <-
make_create_data(event_as_data, Map.merge(audience, additional)) do
{:ok, event, create_data}
end
end
@spec create_comment(map(), map()) :: {:ok, map()}
defp create_comment(args, additional) do
with args <- prepare_args_for_comment(args),
{:ok, %Comment{} = comment} <- Events.create_comment(args),
comment_as_data <- Convertible.model_to_as(comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(
comment.actor,
args.mentions,
args.in_reply_to_comment,
comment.visibility
),
create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, comment, create_data}
end
end
@spec create_group(map(), map()) :: {:ok, map()}
defp create_group(args, additional) do
with args <- prepare_args_for_group(args),
{:ok, %Actor{type: :Group} = group} <- Actors.create_group(args),
group_as_data <- Convertible.model_to_as(group),
audience <-
Audience.calculate_to_and_cc_from_mentions(
args.creator_actor,
[],
nil,
:public
),
create_data <-
make_create_data(group_as_data, Map.merge(audience, additional)) do
{:ok, group, create_data}
end
end
@spec update_event(Event.t(), map(), map()) ::
{:ok, Event.t(), Activity.t()} | any()
defp update_event(
%Event{} = old_event,
args,
additional
) do
with args <- prepare_args_for_event(args),
{:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
event_as_data <- Convertible.model_to_as(new_event),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_event.organizer_actor,
Map.get(args, :mentions, []),
nil,
new_event.visibility
),
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
{:ok, new_event, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec update_actor(Actor.t(), map(), map()) ::
{:ok, Actor.t(), Activity.t()} | any()
defp update_actor(%Actor{} = old_actor, args, additional) do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
actor_as_data <- Convertible.model_to_as(new_actor),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_actor,
[],
nil,
:public
),
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
{:ok, new_actor, update_data}
end
end
@spec accept_follow(Follower.t(), map(), map()) ::
{:ok, Follower.t(), Activity.t()} | any()
defp accept_follow(
%Follower{} = follower,
args,
additional
) do
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, args),
follower_as_data <- Convertible.model_to_as(follower),
audience <-
Audience.calculate_to_and_cc_from_mentions(follower.target_actor),
update_data <-
make_update_data(
follower_as_data,
Map.merge(Map.merge(audience, additional), %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}"
})
) do
{:ok, follower, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec accept_join(Participant.t(), map(), map()) ::
{:ok, Participant.t(), Activity.t()} | any()
defp accept_join(
%Participant{} = participant,
args,
additional \\ %{}
) do
with {:ok, %Participant{} = participant} <- Events.update_participant(participant, args),
participant_as_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant.actor),
update_data <-
make_accept_join_data(
participant_as_data,
Map.merge(Map.merge(audience, additional), %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
})
) do
{:ok, participant, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
# Prepare and sanitize arguments for events
defp prepare_args_for_event(args) do
# If title is not set: we are not updating it
args =
if Map.has_key?(args, :title) && !is_nil(args.title),
do: Map.update(args, :title, "", &String.trim(HtmlSanitizeEx.strip_tags(&1))),
else: args
# If we've been given a description (we might not get one if updating)
# sanitize it, HTML it, and extract tags & mentions from it
args =
if Map.has_key?(args, :description) && !is_nil(args.description) do
{description, mentions, tags} =
APIUtils.make_content_html(
String.trim(args.description),
Map.get(args, :tags, []),
"text/html"
)
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
Map.merge(args, %{
description: description,
mentions: mentions,
tags: tags
})
else
args
end
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
end
# Prepare and sanitize arguments for comments
defp prepare_args_for_comment(args) do
with in_reply_to_comment <-
args |> Map.get(:in_reply_to_comment_id) |> Events.get_comment(),
args <- Map.update(args, :visibility, :public, & &1),
{text, mentions, tags} <-
APIUtils.make_content_html(
args |> Map.get(:text, "") |> String.trim(),
# Can't put additional tags on a comment
[],
"text/html"
),
tags <- ConverterUtils.fetch_tags(tags),
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
args <-
Map.merge(args, %{
actor_id: Map.get(args, :actor_id),
text: text,
mentions: mentions,
tags: tags,
in_reply_to_comment: in_reply_to_comment,
in_reply_to_comment_id:
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id))
}) do
args
end
end
defp prepare_args_for_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
summary <- args |> Map.get(:summary, "") |> String.trim(),
{summary, _mentions, _tags} <-
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
%{args | preferred_username: preferred_username, summary: summary}
end
end
end end

View File

@ -0,0 +1,98 @@
defmodule Mobilizon.Service.ActivityPub.Audience do
@moduledoc """
Tools for calculating content audience
"""
alias Mobilizon.Actors.Actor
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@doc """
Determines the full audience based on mentions for a public audience
Audience is:
* `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()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :public) do
to = [@ap_public | mentions]
cc = [actor.followers_url]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a unlisted audience
Audience is:
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
* `cc` : public
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :unlisted) do
to = [actor.followers_url | mentions]
cc = [@ap_public]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a private audience
Audience is:
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :private) do
{to, cc} = get_to_and_cc(actor, mentions, in_reply_to, :direct)
{[actor.followers_url | to], cc}
end
@doc """
Determines the full audience based on mentions based on a direct audience
Audience is:
* `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, in_reply_to, :direct) do
if in_reply_to do
{Enum.uniq([in_reply_to.actor | mentions]), []}
else
{mentions, []}
end
end
def get_to_and_cc(_actor, mentions, _in_reply_to, {:list, _}) do
{mentions, []}
end
# def get_addressed_actors(_, to) when is_list(to) do
# Actors.get(to)
# end
def get_addressed_actors(mentioned_users, _), do: mentioned_users
def calculate_to_and_cc_from_mentions(
actor,
mentions \\ [],
in_reply_to \\ nil,
visibility \\ :public
) do
with mentioned_actors <- for({_, mentioned_actor} <- mentions, do: mentioned_actor.url),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
{to, cc} <- get_to_and_cc(actor, addressed_actors, in_reply_to, visibility) do
%{"to" => to, "cc" => cc}
end
end
end

View File

@ -37,17 +37,18 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
"url" => object["image"]["url"] "url" => object["image"]["url"]
} }
%{ {:ok,
"type" => String.to_existing_atom(object["type"]), %{
"preferred_username" => object["preferredUsername"], "type" => String.to_existing_atom(object["type"]),
"summary" => object["summary"], "preferred_username" => object["preferredUsername"],
"url" => object["id"], "summary" => object["summary"],
"name" => object["name"], "url" => object["id"],
"avatar" => avatar, "name" => object["name"],
"banner" => banner, "avatar" => avatar,
"keys" => object["publicKey"]["publicKeyPem"], "banner" => banner,
"manually_approves_followers" => object["manuallyApprovesFollowers"] "keys" => object["publicKey"]["publicKeyPem"],
} "manually_approves_followers" => object["manuallyApprovesFollowers"]
}}
end end
@doc """ @doc """

View File

@ -11,6 +11,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible} alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger require Logger
@ -26,57 +27,67 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @impl Converter
@spec as_to_model_data(map) :: map @spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
def as_to_model_data(object) do def as_to_model_data(object) do
{:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"]) Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug("Inserting full comment")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
data = %{ with {:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
"text" => object["content"], {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
"url" => object["id"], {:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])} do
"actor_id" => actor_id, Logger.debug("Inserting full comment")
"in_reply_to_comment_id" => nil, Logger.debug(inspect(object))
"event_id" => nil,
"uuid" => object["uuid"]
}
# We fetch the parent object data = %{
Logger.debug("We're fetching the parent object") text: object["content"],
url: object["id"],
actor_id: actor_id,
in_reply_to_comment_id: nil,
event_id: nil,
uuid: object["uuid"],
tags: tags,
mentions: mentions
}
data = # We fetch the parent object
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil && Logger.debug("We're fetching the parent object")
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do data =
# Reply to an event (Event) if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
{:ok, %Event{id: id}} -> object["inReplyTo"] != "" do
Logger.debug("Parent object is an event") Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
data |> Map.put("event_id", id)
# Reply to a comment (Comment) case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
{:ok, %CommentModel{id: id} = comment} -> # Reply to an event (Event)
Logger.debug("Parent object is another comment") {:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put(:event_id, id)
data # Reply to a comment (Comment)
|> Map.put("in_reply_to_comment_id", id) {:ok, %CommentModel{id: id} = comment} ->
|> Map.put("origin_comment_id", comment |> CommentModel.get_thread_id()) Logger.debug("Parent object is another comment")
# Anything else is kind of a MP data
{:error, parent} -> |> Map.put(:in_reply_to_comment_id, id)
Logger.debug("Parent object is something we don't handle") |> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
Logger.debug(inspect(parent))
data # Anything else is kind of a MP
{:error, parent} ->
Logger.debug("Parent object is something we don't handle")
Logger.debug(inspect(parent))
data
end
else
Logger.debug("No parent object for this comment")
data
end end
else
Logger.debug("No parent object for this comment")
data {:ok, data}
end else
err ->
data {:error, err}
end
end end
@doc """ @doc """
@ -85,14 +96,22 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
@impl Converter @impl Converter
@spec model_to_as(CommentModel.t()) :: map @spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do def model_to_as(%CommentModel{} = comment) do
to =
if comment.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [comment.actor.followers_url]
object = %{ object = %{
"type" => "Note", "type" => "Note",
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => to,
"cc" => [],
"content" => comment.text, "content" => comment.text,
"actor" => comment.actor.url, "actor" => comment.actor.url,
"attributedTo" => comment.actor.url, "attributedTo" => comment.actor.url,
"uuid" => comment.uuid, "uuid" => comment.uuid,
"id" => comment.url "id" => comment.url,
"tag" =>
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
} }
if comment.in_reply_to_comment do if comment.in_reply_to_comment do

View File

@ -6,15 +6,17 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
internal one, and back. internal one, and back.
""" """
alias Mobilizon.{Actors, Addresses, Events, Media} alias Mobilizon.{Addresses, Media}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.{EventOptions, Tag} alias Mobilizon.Events.EventOptions
alias Mobilizon.Media.Picture alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils} alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger require Logger
@ -30,16 +32,16 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @impl Converter
@spec as_to_model_data(map) :: map @spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
def as_to_model_data(object) do def as_to_model_data(object) do
Logger.debug("event as_to_model_data") Logger.debug("event as_to_model_data")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
with {:actor, {:ok, %Actor{id: actor_id}}} <- with {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, Actors.get_actor_by_url(object["actor"])}, {:actor, ActivityPub.get_or_fetch_actor_by_url(object["actor"])},
{:address, address_id} <- {:address, address_id} <-
{:address, get_address(object["location"])}, {:address, get_address(object["location"])},
{:tags, tags} <- {:tags, fetch_tags(object["tag"])}, {:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)}, {:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do {:options, options} <- {:options, get_options(object)} do
picture_id = picture_id =
@ -58,26 +60,27 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
end end
entity = %{ entity = %{
"title" => object["name"], title: object["name"],
"description" => object["content"], description: object["content"],
"organizer_actor_id" => actor_id, organizer_actor_id: actor_id,
"picture_id" => picture_id, picture_id: picture_id,
"begins_on" => object["startTime"], begins_on: object["startTime"],
"ends_on" => object["endTime"], ends_on: object["endTime"],
"category" => object["category"], category: object["category"],
"visibility" => visibility, visibility: visibility,
"join_options" => object["joinOptions"], join_options: Map.get(object, "joinOptions", "free"),
"status" => object["status"], options: options,
"online_address" => object["onlineAddress"], status: object["status"],
"phone_address" => object["phoneAddress"], online_address: object["onlineAddress"],
"draft" => object["draft"] || false, phone_address: object["phoneAddress"],
"url" => object["id"], draft: object["draft"] || false,
"uuid" => object["uuid"], url: object["id"],
"tags" => tags, uuid: object["uuid"],
"physical_address_id" => address_id tags: tags,
physical_address_id: address_id
} }
{:ok, Map.put(entity, "options", options)} {:ok, entity}
else else
error -> error ->
{:error, error} {:error, error}
@ -111,7 +114,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"startTime" => event.begins_on |> date_to_string(), "startTime" => event.begins_on |> date_to_string(),
"joinOptions" => to_string(event.join_options), "joinOptions" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(), "endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(), "tag" => event.tags |> ConverterUtils.build_tags(),
"draft" => event.draft, "draft" => event.draft,
"id" => event.url, "id" => event.url,
"url" => event.url "url" => event.url
@ -181,32 +184,6 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
end end
end end
@spec fetch_tags([String.t()]) :: [String.t()]
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
acc ++ [tag]
else
_err ->
acc
end
end)
end
@spec build_tags([String.t()]) :: String.t()
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
@ap_public "https://www.w3.org/ns/activitystreams#Public" @ap_public "https://www.w3.org/ns/activitystreams#Public"
defp get_visibility(object) do defp get_visibility(object) do

View File

@ -0,0 +1,36 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Follower do
@moduledoc """
Participant converter.
This module allows to convert followers from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Follower, as: FollowerModel
alias Mobilizon.Service.ActivityPub.Convertible
alias Mobilizon.Actors.Actor
defimpl Convertible, for: FollowerModel do
alias Mobilizon.Service.ActivityPub.Converter.Follower, as: FollowerConverter
defdelegate model_to_as(follower), to: FollowerConverter
end
@doc """
Convert an follow struct to an ActivityStream representation.
"""
@spec model_to_as(FollowerModel.t()) :: map
def model_to_as(
%FollowerModel{actor: %Actor{} = actor, target_actor: %Actor{} = target_actor} = follower
) do
%{
"type" => "Follow",
"actor" => actor.url,
"to" => [target_actor.url],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => target_actor.url,
"id" => follower.url
}
end
end

View File

@ -0,0 +1,100 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Utils do
@moduledoc """
Various utils for converters
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Tag
alias Mobilizon.Mention
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Repo
require Logger
@spec fetch_tags([String.t()]) :: [Tag.t()]
def fetch_tags(tags) when is_list(tags) do
Logger.debug("fetching tags")
Enum.reduce(tags, [], &fetch_tag/2)
end
@spec fetch_mentions([map()]) :: [map()]
def fetch_mentions(mentions) when is_list(mentions) do
Logger.debug("fetching mentions")
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
end
def fetch_address(%{id: id}) do
with {id, ""} <- Integer.parse(id) do
%{id: id}
end
end
def fetch_address(address) when is_map(address) do
address
end
@spec build_tags([Tag.t()]) :: [Map.t()]
def build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
"name" => "##{tag.title}",
"type" => "Hashtag"
}
end)
end
def build_mentions(mentions) do
Enum.map(mentions, fn %Mention{} = mention ->
if Ecto.assoc_loaded?(mention.actor) do
build_mention(mention.actor)
else
build_mention(Repo.preload(mention, [:actor]).actor)
end
end)
end
defp build_mention(%Actor{} = actor) do
%{
"href" => actor.url,
"name" => "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(actor)}",
"type" => "Mention"
}
end
defp fetch_tag(tag, acc) when is_map(tag) do
case tag["type"] do
"Hashtag" ->
acc ++ [%{title: tag}]
_err ->
acc
end
end
defp fetch_tag(tag, acc) when is_bitstring(tag) do
acc ++ [%{title: tag}]
end
@spec create_mention(map(), list()) :: list()
defp create_mention(%Actor{id: actor_id} = _mention, acc) do
acc ++ [%{actor_id: actor_id}]
end
@spec create_mention(map(), list()) :: list()
defp create_mention(mention, acc) when is_map(mention) do
with true <- mention["type"] == "Mention",
{:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(mention["href"]) do
acc ++ [%{actor_id: actor_id}]
else
_err ->
acc
end
end
@spec create_mention({String.t(), map()}, list()) :: list()
defp create_mention({_, mention}, acc) when is_map(mention) do
create_mention(mention, acc)
end
end

View File

@ -26,7 +26,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def follow(target_instance) do def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(), with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do {:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}") Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
@ -39,7 +39,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def unfollow(target_instance) do def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(), with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do {:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}") Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
@ -52,7 +52,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def accept(target_instance) do def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(), with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance), {:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do {:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity} {:ok, activity}
end end
@ -60,7 +60,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
# def reject(target_instance) do # def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(), # with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance), # {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_actor_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do # {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity} # {:ok, activity}
# end # end

View File

@ -13,9 +13,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event, Participant} alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils, Visibility} alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Utils}
alias MobilizonWeb.Email.Participation alias MobilizonWeb.Email.Participation
import Mobilizon.Service.ActivityPub.Utils
require Logger require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do def get_actor(%{"actor" => actor}) when is_binary(actor) do
@ -138,59 +140,65 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do @doc """
Handles a `Create` activity for `Note` (comments) objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the comment model format (it also finds and inserts tags)
* Get (by it's URL) or create the comment with this data
* Insert eventual mentions in the database
* Convert the comment back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the comment object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
Logger.info("Handle incoming to create notes") Logger.info("Handle incoming to create notes")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do with {:ok, object_data} <-
Logger.debug("found actor") object |> fix_object() |> Converter.Comment.as_to_model_data(),
Logger.debug(inspect(actor)) {:existing_comment, {:error, :comment_not_found}} <-
{:existing_comment, Events.get_comment_from_url_with_preload(object_data.url)},
params = %{ {:ok, %Activity{} = activity, %Comment{} = comment} <-
to: data["to"], ActivityPub.create(:comment, object_data, false) do
object: object |> fix_object, {:ok, activity, comment}
actor: actor, else
local: false, {:existing_comment, {:ok, %Comment{} = comment}} ->
published: data["published"], {:ok, nil, comment}
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
end end
end end
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object} = data) do @doc """
Handles a `Create` activity for `Event` objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the event model format (it also finds and inserts tags)
* Get (by it's URL) or create the event with this data
* Insert eventual mentions in the database
* Convert the event back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the event object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
Logger.info("Handle incoming to create event") Logger.info("Handle incoming to create event")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do with {:ok, object_data} <-
Logger.debug("found actor") object |> fix_object() |> Converter.Event.as_to_model_data(),
Logger.debug(inspect(actor)) {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <-
params = %{ ActivityPub.create(:event, object_data, false) do
to: data["to"], {:ok, activity, event}
object: object |> fix_object, else
actor: actor, {:existing_event, %Event{} = event} -> {:ok, nil, event}
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
end end
end end
def handle_incoming( def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do ) do
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_by_url(followed, true), with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_actor_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_by_url(follower), {:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
{:ok, activity, object} {:ok, activity, object}
else else
@ -209,8 +217,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data } = data
) do ) do
with actor_url <- get_actor(data), with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <- {:object_not_found, {:ok, %Activity{} = activity, object}} <-
{:object_not_found, {:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) || do_handle_incoming_accept_following(accepted_object, actor) ||
do_handle_incoming_accept_join(accepted_object, actor)} do do_handle_incoming_accept_join(accepted_object, actor)} do
@ -238,7 +246,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data %{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do ) do
with actor_url <- get_actor(data), with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <- {:object_not_found, {:ok, activity, object}} <-
{:object_not_found, {:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) || do_handle_incoming_reject_following(rejected_object, actor) ||
@ -278,13 +286,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end # end
# # # #
def handle_incoming( def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => _id} = data
) do ) do
with actor <- get_actor(data), with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor), # TODO: Is the following line useful?
{:ok, %Actor{} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
:ok <- Logger.debug("Fetching contained object"),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
public <- Visibility.is_public?(data), :ok <- Logger.debug("Handling contained object"),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do create_data <-
make_create_data(object),
:ok <- Logger.debug(inspect(object)),
{:ok, _activity, object} <- handle_incoming(create_data),
:ok <- Logger.debug("Finished processing contained object"),
{:ok, activity} <- ActivityPub.create_activity(data, false) do
{:ok, activity, object} {:ok, activity, object}
else else
e -> e ->
@ -293,21 +308,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( def handle_incoming(%{
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} = "type" => "Update",
data "object" => %{"type" => object_type} = object,
) "actor" => _actor_id
})
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
case Actors.get_actor_by_url(object["id"]) do with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
{:ok, %Actor{url: actor_url}} -> {:ok, object_data} <-
ActivityPub.update(%{ object |> fix_object() |> Converter.Actor.as_to_model_data(),
local: false, {:ok, %Activity{} = activity, %Actor{} = new_actor} <-
to: data["to"] || [], ActivityPub.update(:actor, old_actor, object_data, false) do
cc: data["cc"] || [], {:ok, activity, new_actor}
object: object, else
actor: actor_url
})
e -> e ->
Logger.debug(inspect(e)) Logger.debug(inspect(e))
:error :error
@ -315,24 +328,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
def handle_incoming( def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} = %{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
_update _update
) do ) do
with {:ok, %{"actor" => existing_organizer_actor_url} = existing_event_data} <- with %Event{} = old_event <-
fetch_obj_helper_as_activity_streams(object), Events.get_event_by_url(object["id"]),
object <- Map.merge(existing_event_data, object), {:ok, object_data} <-
{:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(), object |> fix_object() |> Converter.Event.as_to_model_data(),
true <- Utils.get_url(existing_organizer_actor_url) == actor_url do {:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(%{ ActivityPub.update(:event, old_event, object_data, false) do
local: false, {:ok, activity, new_event}
to: object["to"] || [],
cc: object["cc"] || [],
object: object,
actor: actor_url
})
else else
e -> e ->
Logger.debug(inspect(e)) Logger.error(inspect(e))
:error :error
end end
end end
@ -350,7 +358,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data } = data
) do ) do
with actor <- get_actor(data), with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <- {:ok, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
@ -454,7 +462,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# } = data # } = data
# ) do # ) do
# with actor <- get_actor(data), # with actor <- get_actor(data),
# %Actor{} = actor <- ActivityPub.get_or_fetch_by_url(actor), # %Actor{} = actor <- ActivityPub.get_or_fetch_actor_by_url(actor),
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id), # {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do # {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity} # {:ok, activity}
@ -472,23 +480,16 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Handle incoming `Accept` activities wrapping a `Follow` activity Handle incoming `Accept` activities wrapping a `Follow` activity
""" """
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
with {:follow, with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
{:follow, get_follow(follow_object)}, {:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <- {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept( ActivityPub.accept(
%{ :follow,
to: [follower.url], follow,
actor: actor.url, %{approved: true},
object: follow_object, false
local: false ) do
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity, follow} {:ok, activity, follow}
else else
{:follow, _} -> {:follow, _} ->
@ -546,26 +547,18 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event # Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event, with {:join_event, {:ok, %Participant{role: :not_approved, event: event} = participant}} <-
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
{:join_event, get_participant(join_object)}, {:join_event, get_participant(join_object)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ? # TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity # Or maybe for groups it's the group that sends the Accept activity
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id}, {:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <- {:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept( ActivityPub.accept(
%{ :join,
to: [actor.url], participant,
actor: actor_accepting.url, %{role: :participant},
object: join_object, false
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{join_id}"
), ),
{:ok, %Participant{role: :participant}} <-
Events.update_participant(participant, %{"role" => :participant}),
:ok <- :ok <-
Participation.send_emails_to_local_user(participant) do Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant} {:ok, activity, participant}
@ -684,7 +677,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def prepare_object(object) do def prepare_object(object) do
object object
# |> set_sensitive # |> set_sensitive
|> add_hashtags # |> add_hashtags
|> add_mention_tags |> add_mention_tags
# |> add_emoji_tags # |> add_emoji_tags
|> add_attributed_to |> add_attributed_to
@ -781,6 +774,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
def add_mention_tags(object) do def add_mention_tags(object) do
Logger.debug("add mention tags")
Logger.debug(inspect(object))
recipients = recipients =
(object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"] (object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"]
@ -795,7 +791,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end) end)
|> Enum.filter(& &1) |> Enum.filter(& &1)
|> Enum.map(fn actor -> |> Enum.map(fn actor ->
%{"type" => "Mention", "href" => actor.url, "name" => "@#{actor.preferred_username}"} %{
"type" => "Mention",
"href" => actor.url,
"name" => "@#{Actor.preferred_username_and_domain(actor)}"
}
end) end)
tags = object["tag"] || [] tags = object["tag"] || []
@ -854,6 +854,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any() @spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(object) do def fetch_obj_helper(object) do
Logger.debug("fetch_obj_helper")
Logger.debug("Fetching object #{inspect(object)}") Logger.debug("Fetching object #{inspect(object)}")
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
@ -867,6 +868,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
def fetch_obj_helper_as_activity_streams(object) do def fetch_obj_helper_as_activity_streams(object) do
Logger.debug("fetch_obj_helper_as_activity_streams")
with {:ok, object} <- fetch_obj_helper(object) do with {:ok, object} <- fetch_obj_helper(object) do
{:ok, Convertible.model_to_as(object)} {:ok, Convertible.model_to_as(object)}
end end

View File

@ -238,8 +238,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """ @doc """
Save picture data from %Plug.Upload{} and return AS Link data. Save picture data from %Plug.Upload{} and return AS Link data.
""" """
def make_picture_data(%Plug.Upload{} = picture) do def make_picture_data(%Plug.Upload{} = picture, opts) do
case MobilizonWeb.Upload.store(picture) do case MobilizonWeb.Upload.store(picture, opts) do
{:ok, picture} -> {:ok, picture} ->
picture picture
@ -636,18 +636,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Make create activity data Make create activity data
""" """
@spec make_create_data(map(), map()) :: map() @spec make_create_data(map(), map()) :: map()
def make_create_data(params, additional \\ %{}) do def make_create_data(object, additional \\ %{}) do
Logger.debug("Making create data") Logger.debug("Making create data")
Logger.debug(inspect(params)) Logger.debug(inspect(object))
published = params.published || make_date() Logger.debug(inspect(additional))
%{ %{
"type" => "Create", "type" => "Create",
"to" => params.to |> Enum.uniq(), "to" => object["to"],
"actor" => params.actor.url, "cc" => object["cc"],
"object" => params.object, "actor" => object["actor"],
"published" => published, "object" => object,
"id" => params.object["id"] <> "/activity" "published" => make_date(),
"id" => object["id"] <> "/activity"
}
|> Map.merge(additional)
end
@doc """
Make update activity data
"""
@spec make_update_data(map(), map()) :: map()
def make_update_data(object, additional \\ %{}) do
Logger.debug("Making update data")
Logger.debug(inspect(object))
Logger.debug(inspect(additional))
%{
"type" => "Update",
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"id" => object["id"] <> "/activity"
} }
|> Map.merge(additional) |> Map.merge(additional)
end end
@ -688,6 +709,22 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
} }
end end
@doc """
Make accept join activity data
"""
@spec make_accept_join_data(map(), map()) :: map()
def make_accept_join_data(object, additional \\ %{}) do
%{
"type" => "Accept",
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"id" => object["id"] <> "/activity"
}
|> Map.merge(additional)
end
@doc """ @doc """
Converts PEM encoded keys to a public key representation Converts PEM encoded keys to a public key representation
""" """

View File

@ -53,7 +53,7 @@ defmodule Mobilizon.Service.Federator do
Logger.debug(inspect(params)) Logger.debug(inspect(params))
case Transmogrifier.handle_incoming(params) do case Transmogrifier.handle_incoming(params) do
{:ok, activity, _} -> {:ok, activity, _data} ->
{:ok, activity} {:ok, activity}
%Activity{} -> %Activity{} ->

View File

@ -52,7 +52,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
@spec get_public_key_for_url(String.t()) :: @spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error} {:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
def get_public_key_for_url(url) do def get_public_key_for_url(url) do
with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_by_url(url), with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_actor_by_url(url),
{:ok, public_key} <- prepare_public_key(keys) do {:ok, public_key} <- prepare_public_key(keys) do
{:ok, public_key} {:ok, public_key}
else else

View File

@ -1,12 +1,5 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddEagerMaterializedViewForSearchingEvents do defmodule Mobilizon.Storage.Repo.Migrations.AddEagerMaterializedViewForSearchingEvents do
use Ecto.Migration use Ecto.Migration
import Ecto.Query
alias Mobilizon.Storage.Repo
alias Mobilizon.Service.Search
alias Mobilizon.Events.Event
require Logger
def up do def up do
create table(:event_search, primary_key: false) do create table(:event_search, primary_key: false) do
@ -28,35 +21,6 @@ defmodule Mobilizon.Storage.Repo.Migrations.AddEagerMaterializedViewForSearching
# to support updating CONCURRENTLY # to support updating CONCURRENTLY
create(unique_index("event_search", [:id])) create(unique_index("event_search", [:id]))
flush()
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts("\nStarting setting up search for #{nb_events} events, this can take a while…\n")
insert_search_event(events, nb_events)
end
defp insert_search_event([%Event{url: url} = event | events], nb_events) do
with {:ok, _} <- Search.insert_search_event(event) do
Logger.debug("Added event #{url} to the search")
else
{:error, res} ->
Logger.error("Error while adding event #{url} to the search: #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_search_event(events, nb_events)
end
defp insert_search_event([], nb_events) do
IO.puts("\nFinished setting up search for #{nb_events} events!\n")
end end
def down do def down do

View File

@ -0,0 +1,15 @@
defmodule Mobilizon.Storage.Repo.Migrations.MoveParticipantsStatsToEvent do
use Ecto.Migration
def up do
alter table(:events) do
add(:participant_stats, :map)
end
end
def down do
alter table(:events) do
remove(:participant_stats)
end
end
end

View File

@ -0,0 +1,14 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddTagsToComments do
use Ecto.Migration
def up do
create table(:comments_tags, primary_key: false) do
add(:comment_id, references(:comments, on_delete: :delete_all), primary_key: true)
add(:tag_id, references(:tags, on_delete: :nilify_all), primary_key: true)
end
end
def down do
drop(table(:comments_tags))
end
end

View File

@ -0,0 +1,16 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddMentionTables do
use Ecto.Migration
def change do
create table(:mentions) do
add(:silent, :boolean, default: false, null: false)
add(:actor_id, references(:actors, on_delete: :delete_all), null: false)
add(:event_id, references(:events, on_delete: :delete_all), null: true)
add(:comment_id, references(:comments, on_delete: :delete_all), null: true)
timestamps()
end
create(index(:mentions, [:actor_id]))
end
end

View File

@ -0,0 +1,8 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddUniqueIndexOnURLs do
use Ecto.Migration
def change do
create(unique_index(:events, [:url]))
create(unique_index(:comments, [:url]))
end
end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api # source: http://localhost:4000/api
# timestamp: Mon Oct 14 2019 19:26:36 GMT+0200 (Central European Summer Time) # timestamp: Wed Oct 30 2019 17:12:28 GMT+0100 (Central European Standard Time)
schema { schema {
query: RootQueryType query: RootQueryType
@ -690,17 +690,26 @@ enum ParticipantRoleEnum {
} }
type ParticipantStats { type ParticipantStats {
"""The number of administrators"""
administrator: Int
"""The number of creators"""
creator: Int
"""The number of approved participants""" """The number of approved participants"""
approved: Int going: Int
"""The number of moderators"""
moderator: Int
"""The number of not approved participants"""
notApproved: Int
"""The number of simple participants (excluding creators)""" """The number of simple participants (excluding creators)"""
participants: Int participant: Int
"""The number of rejected participants""" """The number of rejected participants"""
rejected: Int rejected: Int
"""The number of unapproved participants"""
unapproved: Int
} }
""" """
@ -908,7 +917,7 @@ type RootMutationType {
changePassword(newPassword: String!, oldPassword: String!): User changePassword(newPassword: String!, oldPassword: String!): User
"""Create a comment""" """Create a comment"""
createComment(actorUsername: String!, text: String!): Comment createComment(actorId: ID!, text: String!): Comment
"""Create an event""" """Create an event"""
createEvent( createEvent(

View File

@ -28,9 +28,9 @@
"attributedTo": "https://framapiaf.org/users/admin", "attributedTo": "https://framapiaf.org/users/admin",
"cc": [ "cc": [
"https://framapiaf.org/users/admin/followers", "https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "https://framapiaf.org/users/tcit"
], ],
"content": "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span> #moo</p>", "content": "<p><span class=\"h-card\"><a href=\"https://framapiaf.org/users/tcit\" class=\"u-url mention\">@<span>tcit</span></a></span> #moo</p>",
"conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation", "conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation",
"id": "https://framapiaf.org/users/admin/statuses/99512778738411822", "id": "https://framapiaf.org/users/admin/statuses/99512778738411822",
"inReplyTo": null, "inReplyTo": null,
@ -40,8 +40,8 @@
"summary": "cw", "summary": "cw",
"tag": [ "tag": [
{ {
"href": "http://localtesting.pleroma.lol/users/lain", "href": "https://framapiaf.org/users/tcit",
"name": "@lain@localtesting.pleroma.lol", "name": "@tcit@framapiaf.org",
"type": "Mention" "type": "Mention"
}, },
{ {

View File

@ -28,9 +28,9 @@
"attributedTo": "https://framapiaf.org/users/admin", "attributedTo": "https://framapiaf.org/users/admin",
"cc": [ "cc": [
"https://framapiaf.org/users/admin/followers", "https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "https://framapiaf.org/users/tcit"
], ],
"content": "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>", "content": "<p><span class=\"h-card\"><a href=\"https://framapiaf.org/users/tcit\" class=\"u-url mention\">@<span>tcit</span></a></span></p>",
"conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation", "conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation",
"id": "https://framapiaf.org/users/admin/statuses/99512778738411822", "id": "https://framapiaf.org/users/admin/statuses/99512778738411822",
"inReplyTo": null, "inReplyTo": null,
@ -40,8 +40,8 @@
"summary": "cw", "summary": "cw",
"tag": [ "tag": [
{ {
"href": "http://localtesting.pleroma.lol/users/lain", "href": "https://framapiaf.org/users/tcit",
"name": "@lain@localtesting.pleroma.lol", "name": "@tcit@framapiaf.org",
"type": "Mention" "type": "Mention"
} }
], ],

View File

@ -14,7 +14,7 @@
"actor": "https://event1.tcit.fr/@tcit", "actor": "https://event1.tcit.fr/@tcit",
"cc": [ "cc": [
"https://framapiaf.org/users/admin/followers", "https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "https://framapiaf.org/users/tcit"
], ],
"id": "https://event1.tcit.fr/@tcit/events/109ccdfd-ee3e-46e1-a877-6c228763df0c/activity", "id": "https://event1.tcit.fr/@tcit/events/109ccdfd-ee3e-46e1-a877-6c228763df0c/activity",
"object": { "object": {
@ -23,9 +23,9 @@
"startTime": "2018-02-12T14:08:20Z", "startTime": "2018-02-12T14:08:20Z",
"cc": [ "cc": [
"https://framapiaf.org/users/admin/followers", "https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "https://framapiaf.org/users/tcit"
], ],
"content": "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>", "content": "<p><span class=\"h-card\"><a href=\"https://framapiaf.org/users/tcit\" class=\"u-url mention\">@<span>tcit</span></a></span></p>",
"category": "TODO remove me", "category": "TODO remove me",
"id": "https://event1.tcit.fr/@tcit/events/109ccdfd-ee3e-46e1-a877-6c228763df0c", "id": "https://event1.tcit.fr/@tcit/events/109ccdfd-ee3e-46e1-a877-6c228763df0c",
"inReplyTo": null, "inReplyTo": null,
@ -46,8 +46,8 @@
"published": "2018-02-12T14:08:20Z", "published": "2018-02-12T14:08:20Z",
"tag": [ "tag": [
{ {
"href": "http://localtesting.pleroma.lol/users/lain", "href": "https://framapiaf.org/users/tcit",
"name": "@lain@localtesting.pleroma.lol", "name": "@tcit@framapiaf.org",
"type": "Mention" "type": "Mention"
} }
], ],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -99,7 +99,7 @@ defmodule Mobilizon.ActorsTest do
preferred_username: preferred_username, preferred_username: preferred_username,
domain: domain, domain: domain,
avatar: %FileModel{name: picture_name} = _picture avatar: %FileModel{name: picture_name} = _picture
} = _actor} = ActivityPub.get_or_fetch_by_url(@remote_account_url) } = _actor} = ActivityPub.get_or_fetch_actor_by_url(@remote_account_url)
assert picture_name == "avatar" assert picture_name == "avatar"
@ -149,7 +149,7 @@ defmodule Mobilizon.ActorsTest do
test "get_actor_by_name_with_preload!/1 returns the remote actor with its organized events" do test "get_actor_by_name_with_preload!/1 returns the remote actor with its organized events" do
use_cassette "actors/remote_actor_mastodon_tcit" do use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(@remote_account_url) do
assert Actors.get_actor_by_name_with_preload( assert Actors.get_actor_by_name_with_preload(
"#{actor.preferred_username}@#{actor.domain}" "#{actor.preferred_username}@#{actor.domain}"
).organized_events == [] ).organized_events == []
@ -178,7 +178,8 @@ defmodule Mobilizon.ActorsTest do
test "test build_actors_by_username_or_name_page/4 returns actors with similar usernames", test "test build_actors_by_username_or_name_page/4 returns actors with similar usernames",
%{actor: %Actor{id: actor_id}} do %{actor: %Actor{id: actor_id}} do
use_cassette "actors/remote_actor_mastodon_tcit" do use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{id: actor2_id}} <- ActivityPub.get_or_fetch_by_url(@remote_account_url) do with {:ok, %Actor{id: actor2_id}} <-
ActivityPub.get_or_fetch_actor_by_url(@remote_account_url) do
%Page{total: 2, elements: actors} = %Page{total: 2, elements: actors} =
Actors.build_actors_by_username_or_name_page("tcit", [:Person]) Actors.build_actors_by_username_or_name_page("tcit", [:Person])
@ -253,12 +254,11 @@ defmodule Mobilizon.ActorsTest do
} }
{:ok, data} = MobilizonWeb.Upload.store(file) {:ok, data} = MobilizonWeb.Upload.store(file)
url = hd(data["url"])["href"]
assert {:ok, actor} = assert {:ok, actor} =
Actors.update_actor( Actors.update_actor(
actor, actor,
Map.put(@update_attrs, :avatar, %{name: file.filename, url: url}) Map.put(@update_attrs, :avatar, %{name: file.filename, url: data.url})
) )
assert %Actor{} = actor assert %Actor{} = actor

View File

@ -115,7 +115,7 @@ defmodule Mobilizon.EventsTest do
end end
test "create_event/1 with invalid data returns error changeset" do test "create_event/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Events.create_event(@invalid_attrs) assert {:error, :insert, %Ecto.Changeset{}, _} = Events.create_event(@invalid_attrs)
end end
test "update_event/2 with valid data updates the event", %{event: event} do test "update_event/2 with valid data updates the event", %{event: event} do
@ -128,7 +128,7 @@ defmodule Mobilizon.EventsTest do
end end
test "update_event/2 with invalid data returns error changeset", %{event: event} do test "update_event/2 with invalid data returns error changeset", %{event: event} do
assert {:error, %Ecto.Changeset{}} = Events.update_event(event, @invalid_attrs) assert {:error, :update, %Ecto.Changeset{}, _} = Events.update_event(event, @invalid_attrs)
assert event.title == Events.get_event!(event.id).title assert event.title == Events.get_event!(event.id).title
end end
@ -345,7 +345,8 @@ defmodule Mobilizon.EventsTest do
end end
test "create_participant/1 with invalid data returns error changeset" do test "create_participant/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Events.create_participant(@invalid_attrs) assert {:error, :participant, %Ecto.Changeset{}, _} =
Events.create_participant(@invalid_attrs)
end end
test "update_participant/2 with valid data updates the participant", %{ test "update_participant/2 with valid data updates the participant", %{

View File

@ -14,12 +14,10 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.HTTPSignatures.Signature
alias MobilizonWeb.ActivityPub.ActorView @activity_pub_public_audience "https://www.w3.org/ns/activitystreams#Public"
setup_all do setup_all do
HTTPoison.start() HTTPoison.start()
@ -53,7 +51,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "returns an actor from url" do test "returns an actor from url" do
use_cassette "activity_pub/fetch_framapiaf.org_users_tcit" do use_cassette "activity_pub/fetch_framapiaf.org_users_tcit" do
assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} = assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} =
ActivityPub.get_or_fetch_by_url("https://framapiaf.org/users/tcit") ActivityPub.get_or_fetch_actor_by_url("https://framapiaf.org/users/tcit")
end end
end end
end end
@ -165,28 +163,15 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "it creates an update activity with the new actor data" do test "it creates an update activity with the new actor data" do
actor = insert(:actor) actor = insert(:actor)
actor_data = ActorView.render("actor.json", %{actor: actor}) actor_data = %{summary: @updated_actor_summary}
actor_data = Map.put(actor_data, "summary", @updated_actor_summary)
{:ok, update, updated_actor} = {:ok, update, _} = ActivityPub.update(:actor, actor, actor_data, false)
ActivityPub.update(%{
actor: actor_data["url"],
to: [actor.url <> "/followers"],
cc: [],
object: actor_data
})
assert update.data["actor"] == actor.url assert update.data["actor"] == actor.url
assert update.data["to"] == [actor.url <> "/followers"] assert update.data["to"] == [@activity_pub_public_audience]
assert update.data["object"]["id"] == actor_data["id"] assert update.data["object"]["id"] == actor.url
assert update.data["object"]["type"] == actor_data["type"] assert update.data["object"]["type"] == "Person"
assert update.data["object"]["summary"] == @updated_actor_summary assert update.data["object"]["summary"] == @updated_actor_summary
refute updated_actor.summary == actor.summary
{:ok, %Actor{} = database_actor} = Mobilizon.Actors.get_actor_by_url(actor.url)
assert database_actor.summary == @updated_actor_summary
assert database_actor.preferred_username == actor.preferred_username
end end
@updated_start_time DateTime.utc_now() |> DateTime.truncate(:second) @updated_start_time DateTime.utc_now() |> DateTime.truncate(:second)
@ -194,28 +179,15 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "it creates an update activity with the new event data" do test "it creates an update activity with the new event data" do
actor = insert(:actor) actor = insert(:actor)
event = insert(:event, organizer_actor: actor) event = insert(:event, organizer_actor: actor)
event_data = Converter.Event.model_to_as(event) event_data = %{begins_on: @updated_start_time}
event_data = Map.put(event_data, "startTime", @updated_start_time)
{:ok, update, updated_event} = {:ok, update, _} = ActivityPub.update(:event, event, event_data)
ActivityPub.update(%{
actor: actor.url,
to: [actor.url <> "/followers"],
cc: [],
object: event_data
})
assert update.data["actor"] == actor.url assert update.data["actor"] == actor.url
assert update.data["to"] == [actor.url <> "/followers"] assert update.data["to"] == [@activity_pub_public_audience]
assert update.data["object"]["id"] == event_data["id"] assert update.data["object"]["id"] == event.url
assert update.data["object"]["type"] == event_data["type"] assert update.data["object"]["type"] == "Event"
assert update.data["object"]["startTime"] == @updated_start_time assert update.data["object"]["startTime"] == DateTime.to_iso8601(@updated_start_time)
refute updated_event.begins_on == event.begins_on
%Event{} = database_event = Mobilizon.Events.get_event_by_url(event.url)
assert database_event.begins_on == @updated_start_time
assert database_event.title == event.title
end end
end end
end end

View File

@ -15,7 +15,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.ActorTest do
describe "AS to Actor" do describe "AS to Actor" do
test "valid as data to model" do test "valid as data to model" do
actor = {:ok, actor} =
ActorConverter.as_to_model_data(%{ ActorConverter.as_to_model_data(%{
"type" => "Person", "type" => "Person",
"preferredUsername" => "test_account" "preferredUsername" => "test_account"

View File

@ -124,10 +124,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [ # assert data["cc"] == [
"https://framapiaf.org/users/admin/followers", # "https://framapiaf.org/users/admin/followers",
"http://mobilizon.com/@tcit" # "http://mobilizon.com/@tcit"
] # ]
assert data["actor"] == "https://framapiaf.org/users/admin" assert data["actor"] == "https://framapiaf.org/users/admin"
@ -136,16 +136,14 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"] assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object["cc"] == [ # assert object["cc"] == [
"https://framapiaf.org/users/admin/followers", # "https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" # "http://localtesting.pleroma.lol/users/lain"
] # ]
assert object["actor"] == "https://framapiaf.org/users/admin" assert object["actor"] == "https://framapiaf.org/users/admin"
assert object["attributedTo"] == "https://framapiaf.org/users/admin" assert object["attributedTo"] == "https://framapiaf.org/users/admin"
assert object["sensitive"] == true
{:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"]) {:ok, %Actor{}} = Actors.get_actor_by_url(object["actor"])
end end
@ -153,6 +151,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org"
assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo" assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo"
end end
@ -347,9 +346,9 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("actor", data["actor"]) |> Map.put("actor", data["actor"])
|> Map.put("object", object) |> Map.put("object", object)
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data) {:ok, %Activity{data: _data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
{:ok, %Actor{} = actor} = Actors.get_actor_by_url(data["actor"]) {:ok, %Actor{} = actor} = Actors.get_actor_by_url(update_data["actor"])
assert actor.name == "nextsoft" assert actor.name == "nextsoft"
assert actor.summary == "<p>Some bio</p>" assert actor.summary == "<p>Some bio</p>"
@ -406,7 +405,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it works for incoming deletes" do test "it works for incoming deletes" do
%Actor{url: actor_url} = actor = insert(:actor) %Actor{url: actor_url} = actor = insert(:actor)
%Comment{url: comment_url} = insert(:comment, actor: actor) %Comment{url: comment_url} = insert(:comment, actor: nil, actor_id: actor.id)
data = data =
File.read!("test/fixtures/mastodon-delete.json") File.read!("test/fixtures/mastodon-delete.json")
@ -622,8 +621,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("object", follow_activity.data["id"]) |> Map.put("object", follow_activity.data["id"])
{:ok, activity, _} = Transmogrifier.handle_incoming(accept_data) {:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"] assert activity.data["object"]["id"] == follow_activity.data["id"]
assert activity.data["object"] =~ "/follow/" assert activity.data["object"]["id"] =~ "/follow/"
assert activity.data["id"] =~ "/accept/follow/" assert activity.data["id"] =~ "/accept/follow/"
{:ok, follower} = Actors.get_actor_by_url(follower.url) {:ok, follower} = Actors.get_actor_by_url(follower.url)
@ -756,8 +755,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("object", participation.url) |> Map.put("object", participation.url)
{:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data) {:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
assert accept_activity.data["object"] == join_activity.data["id"] assert accept_activity.data["object"]["id"] == join_activity.data["id"]
assert accept_activity.data["object"] =~ "/join/" assert accept_activity.data["object"]["id"] =~ "/join/"
assert accept_activity.data["id"] =~ "/accept/join/" assert accept_activity.data["id"] =~ "/accept/join/"
# We don't accept already accepted Accept activities # We don't accept already accepted Accept activities
@ -847,10 +846,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
other_actor = insert(:actor) other_actor = insert(:actor)
{:ok, activity, _} = {:ok, activity, _} =
API.Comments.create_comment( API.Comments.create_comment(%{
actor.preferred_username, actor_id: actor.id,
"hey, @#{other_actor.preferred_username}, how are ya? #2hu" text: "hey, @#{other_actor.preferred_username}, how are ya? #2hu"
) })
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"] object = modified["object"]
@ -883,7 +882,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it adds the json-ld context and the conversation property" do test "it adds the json-ld context and the conversation property" do
actor = insert(:actor) actor = insert(:actor)
{:ok, activity, _} = API.Comments.create_comment(actor.preferred_username, "hey") {:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
@ -893,7 +892,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
actor = insert(:actor) actor = insert(:actor)
{:ok, activity, _} = API.Comments.create_comment(actor.preferred_username, "hey") {:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
@ -903,7 +902,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it strips internal hashtag data" do test "it strips internal hashtag data" do
actor = insert(:actor) actor = insert(:actor)
{:ok, activity, _} = API.Comments.create_comment(actor.preferred_username, "#2hu") {:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "#2hu"})
expected_tag = %{ expected_tag = %{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/2hu", "href" => MobilizonWeb.Endpoint.url() <> "/tags/2hu",
@ -919,7 +918,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it strips internal fields" do test "it strips internal fields" do
actor = insert(:actor) actor = insert(:actor)
{:ok, activity, _} = API.Comments.create_comment(actor.preferred_username, "#2hu") {:ok, activity, _} = API.Comments.create_comment(%{actor_id: actor.id, text: "#2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)

View File

@ -17,12 +17,21 @@ defmodule Mobilizon.Service.ActivityPub.UtilsTest do
describe "make" do describe "make" do
test "comment data from struct" do test "comment data from struct" do
comment = insert(:comment) comment = insert(:comment)
reply = insert(:comment, in_reply_to_comment: comment) tag = insert(:tag, title: "MyTag")
reply = insert(:comment, in_reply_to_comment: comment, tags: [tag])
assert %{ assert %{
"type" => "Note", "type" => "Note",
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"],
"content" => reply.text, "cc" => [],
"tag" => [
%{
"href" => "http://mobilizon.test/tags/#{tag.slug}",
"name" => "#MyTag",
"type" => "Hashtag"
}
],
"content" => "My Comment",
"actor" => reply.actor.url, "actor" => reply.actor.url,
"uuid" => reply.uuid, "uuid" => reply.uuid,
"id" => Routes.page_url(Endpoint, :comment, reply.uuid), "id" => Routes.page_url(Endpoint, :comment, reply.uuid),

View File

@ -18,8 +18,7 @@ defmodule MobilizonWeb.Plugs.UploadedMediaPlugTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
[%{"href" => attachment_url} | _] = data["url"] [attachment_url: data.url]
[attachment_url: attachment_url]
end end
setup_all :upload_file setup_all :upload_file

View File

@ -18,7 +18,7 @@ defmodule MobilizonWeb.Resolvers.CommentResolverTest do
mutation { mutation {
createComment( createComment(
text: "#{@comment.text}", text: "#{@comment.text}",
actor_username: "#{actor.preferred_username}" actor_id: "#{actor.id}"
) { ) {
text, text,
uuid uuid

View File

@ -363,7 +363,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
actor: actor, actor: actor,
user: user user: user
} do } do
address = insert(:address) address = %{street: "I am a street, please believe me", locality: "Where ever"}
mutation = """ mutation = """
mutation { mutation {
@ -383,6 +383,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
title, title,
uuid, uuid,
physicalAddress { physicalAddress {
id,
url, url,
geom, geom,
street street
@ -403,8 +404,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["street"] == assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["street"] ==
address.street address.street
refute json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["url"] == address_url = json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["url"]
address.url address_id = json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["id"]
mutation = """ mutation = """
mutation { mutation {
@ -417,12 +418,13 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
organizer_actor_id: "#{actor.id}", organizer_actor_id: "#{actor.id}",
category: "birthday", category: "birthday",
physical_address: { physical_address: {
url: "#{address.url}" id: "#{address_id}"
} }
) { ) {
title, title,
uuid, uuid,
physicalAddress { physicalAddress {
id,
url, url,
geom, geom,
street street
@ -443,8 +445,11 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["street"] == assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["street"] ==
address.street address.street
assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["id"] ==
address_id
assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["url"] == assert json_response(res, 200)["data"]["createEvent"]["physicalAddress"]["url"] ==
address.url address_url
end end
test "create_event/3 creates an event with an attached picture", %{ test "create_event/3 creates an event with an attached picture", %{
@ -501,7 +506,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
"picture for my event" "picture for my event"
end end
test "create_event/3 creates an event with an picture URL", %{ test "create_event/3 creates an event with an picture ID", %{
conn: conn, conn: conn,
actor: actor, actor: actor,
user: user user: user

View File

@ -2,7 +2,6 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
use MobilizonWeb.ConnCase use MobilizonWeb.ConnCase
alias MobilizonWeb.AbsintheHelpers alias MobilizonWeb.AbsintheHelpers
import Mobilizon.Factory import Mobilizon.Factory
require Logger
@non_existent_username "nonexistent" @non_existent_username "nonexistent"
@new_group_params %{groupname: "new group"} @new_group_params %{groupname: "new group"}
@ -36,7 +35,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] == assert hd(json_response(res, 200)["errors"])["message"] ==
"Actor id is not owned by authenticated user" "Creator actor id is not owned by the current user"
end end
test "create_group/3 creates a group and check a group with this name does not already exist", test "create_group/3 creates a group and check a group with this name does not already exist",

View File

@ -561,8 +561,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
event(uuid: "#{event.uuid}") { event(uuid: "#{event.uuid}") {
uuid, uuid,
participantStats { participantStats {
approved, going,
unapproved, notApproved,
rejected rejected
} }
} }
@ -574,8 +574,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> get("/api", AbsintheHelpers.query_skeleton(query, "event")) |> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid) assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid)
assert json_response(res, 200)["data"]["event"]["participantStats"]["approved"] == 1 assert json_response(res, 200)["data"]["event"]["participantStats"]["going"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["unapproved"] == 0 assert json_response(res, 200)["data"]["event"]["participantStats"]["notApproved"] == 0
assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 0 assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 0
moderator = insert(:actor) moderator = insert(:actor)
@ -586,18 +586,18 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
actor_id: moderator.id actor_id: moderator.id
}) })
unapproved = insert(:actor) not_approved = insert(:actor)
Events.create_participant(%{ Events.create_participant(%{
role: :not_approved, role: :not_approved,
event_id: event.id, event_id: event.id,
actor_id: unapproved.id actor_id: not_approved.id
}) })
Events.create_participant(%{ Events.create_participant(%{
role: :rejected, role: :rejected,
event_id: event.id, event_id: event.id,
actor_id: unapproved.id actor_id: not_approved.id
}) })
query = """ query = """
@ -605,8 +605,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
event(uuid: "#{event.uuid}") { event(uuid: "#{event.uuid}") {
uuid, uuid,
participantStats { participantStats {
approved, going,
unapproved, notApproved,
rejected rejected
} }
} }
@ -618,8 +618,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> get("/api", AbsintheHelpers.query_skeleton(query, "event")) |> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid) assert json_response(res, 200)["data"]["event"]["uuid"] == to_string(event.uuid)
assert json_response(res, 200)["data"]["event"]["participantStats"]["approved"] == 2 assert json_response(res, 200)["data"]["event"]["participantStats"]["going"] == 2
assert json_response(res, 200)["data"]["event"]["participantStats"]["unapproved"] == 1 assert json_response(res, 200)["data"]["event"]["participantStats"]["notApproved"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 1 assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 1
end end
end end

View File

@ -25,9 +25,9 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert %{ assert %{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}], url: url,
"size" => 13_227, content_type: "image/jpeg",
"type" => "Image" size: 13_227
} = data } = data
assert String.starts_with?(url, MobilizonWeb.Endpoint.url() <> "/media/") assert String.starts_with?(url, MobilizonWeb.Endpoint.url() <> "/media/")
@ -46,9 +46,7 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file, base_url: base_url) {:ok, data} = Upload.store(file, base_url: base_url)
assert %{"url" => [%{"href" => url}]} = data assert String.starts_with?(data.url, base_url <> "/media/")
assert String.starts_with?(url, base_url <> "/media/")
end end
test "copies the file to the configured folder with deduping" do test "copies the file to the configured folder with deduping" do
@ -62,7 +60,7 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.Dedupe]) {:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.Dedupe])
assert List.first(data["url"])["href"] == assert data.url ==
MobilizonWeb.Endpoint.url() <> MobilizonWeb.Endpoint.url() <>
"/media/590523d60d3831ec92d05cdd871078409d5780903910efec5cd35ab1b0f19d11.jpg" "/media/590523d60d3831ec92d05cdd871078409d5780903910efec5cd35ab1b0f19d11.jpg"
end end
@ -77,7 +75,7 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data.name == "an [image.jpg"
end end
test "fixes incorrect content type" do test "fixes incorrect content type" do
@ -90,7 +88,7 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.Dedupe]) {:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.Dedupe])
assert hd(data["url"])["mediaType"] == "image/jpeg" assert data.content_type == "image/jpeg"
end end
test "adds missing extension" do test "adds missing extension" do
@ -103,7 +101,7 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data.name == "an [image.jpg"
end end
test "fixes incorrect file extension" do test "fixes incorrect file extension" do
@ -116,7 +114,7 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data.name == "an [image.jpg"
end end
test "don't modify filename of an unknown type" do test "don't modify filename of an unknown type" do
@ -129,7 +127,7 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert data["name"] == "test.txt" assert data.name == "test.txt"
end end
test "copies the file to the configured folder with anonymizing filename" do test "copies the file to the configured folder with anonymizing filename" do
@ -143,7 +141,7 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.AnonymizeFilename]) {:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.AnonymizeFilename])
refute data["name"] == "an [image.jpg" refute data.name == "an [image.jpg"
end end
test "escapes invalid characters in url" do test "escapes invalid characters in url" do
@ -156,9 +154,8 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" assert Path.basename(data.url) == "an%E2%80%A6%20image.jpg"
end end
test "escapes reserved uri characters" do test "escapes reserved uri characters" do
@ -171,9 +168,8 @@ defmodule Mobilizon.UploadTest do
} }
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) == assert Path.basename(data.url) ==
"%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
end end
@ -210,9 +206,9 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
assert %{ assert %{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}], url: url,
"size" => 13_227, size: 13_227,
"type" => "Image" content_type: "image/jpeg"
} = data } = data
assert String.starts_with?(url, MobilizonWeb.Endpoint.url() <> "/media/") assert String.starts_with?(url, MobilizonWeb.Endpoint.url() <> "/media/")

View File

@ -40,7 +40,7 @@ defmodule Mobilizon.Factory do
following_url: Actor.build_url(preferred_username, :following), following_url: Actor.build_url(preferred_username, :following),
inbox_url: Actor.build_url(preferred_username, :inbox), inbox_url: Actor.build_url(preferred_username, :inbox),
outbox_url: Actor.build_url(preferred_username, :outbox), outbox_url: Actor.build_url(preferred_username, :outbox),
user: nil user: build(:user)
} }
end end
@ -100,6 +100,8 @@ defmodule Mobilizon.Factory do
actor: build(:actor), actor: build(:actor),
event: build(:event), event: build(:event),
uuid: uuid, uuid: uuid,
mentions: [],
tags: build_list(3, :tag),
in_reply_to_comment: nil, in_reply_to_comment: nil,
url: Routes.page_url(Endpoint, :comment, uuid) url: Routes.page_url(Endpoint, :comment, uuid)
} }
@ -121,11 +123,13 @@ defmodule Mobilizon.Factory do
physical_address: build(:address), physical_address: build(:address),
visibility: :public, visibility: :public,
tags: build_list(3, :tag), tags: build_list(3, :tag),
mentions: [],
url: Routes.page_url(Endpoint, :event, uuid), url: Routes.page_url(Endpoint, :event, uuid),
picture: insert(:picture), picture: insert(:picture),
uuid: uuid, uuid: uuid,
join_options: :free, join_options: :free,
options: %{} options: %{},
participant_stats: %{}
} }
end end
@ -195,9 +199,10 @@ defmodule Mobilizon.Factory do
{:ok, data} = Upload.store(file) {:ok, data} = Upload.store(file)
%{ %{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}], content_type: "image/jpeg",
"size" => 13_227, name: "image.jpg",
"type" => "Image" url: url,
size: 13_227
} = data } = data
%Mobilizon.Media.File{ %Mobilizon.Media.File{