Merge branch 'feature/refactor-federation' into 'master'

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

Closes #256

See merge request framasoft/mobilizon!298
This commit is contained in:
Thomas Citharel 2019-10-31 11:03:36 +01:00
commit 99677c1b7d
70 changed files with 1945 additions and 1424 deletions

64
CHANGELOG.md Normal file
View File

@ -0,0 +1,64 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Special operations
These two operations couldn't be handled during migrations.
They are optional, but you won't be able to search or get participant stats on existing events if they are not executed.
These commands will be removed in Mobilizon 1.0.0-beta.3.
In order to populate search index for existing events, you need to run the following command (with prod environment):
* `mix mobilizon.setup_search`
In order to move participant stats to the event table for existing events, you need to run the following command (with prod environment):
* `mix mobilizon.move_participant_stats`
### Added
- Implement search engine & service in backend **(read special instructions above)**
- Allow WebP and Gif pics upload
- Optimize uploaded pics
- Make tags clickable, redirecting to search
- Add a different welcome message when coming from registration
- Link to participation page from event page when you are an organizer
- Added a warning on login that everything is deleted regularily
- Updated Occitan translations (Quentin)
- Updated French translations (Gavy, Zilverspar, ty kayn)
- Updated Swedish translations (Anton Strömkvist)
- Upgraded frontend and backend dependencies
### Changed
- Improve Docker setup and docs
- Handle error message difference between user not found and user not confirmed
- Upgrade vue-cli to v4, change the way server params injection is made
- Limit length (20 characters) and number (10) of tags allowed
- Added some backend changes and validation for field length
- Improve some production ipv6 configuration
- Move participant stats to event table **(read special instructions above)**
### Fixed
- Fix event URL validation and check if hostname is correct before showing it
- Fix participations stats on the MyEvents page
- Fix event description lists margin
- Fix Cypress tests
- Fix contribution guide link and improve contribution guide (Joel Takvorian)
- Improve grammar (Damien)
- Fix recursive alias in systemd unit file (Geno)
- Fix multiline display on participants page
- Add polyfill for IntersectionObserver so that it's usable on relatively old browsers
- Fixed crash on Safari on description input by removing `-apple-system` from font-family
- Improve installation docs (mkljczk)
- Limit file uploads to 10MB
- Added missing `setup_db.psql` file (Geno)
- Fixed docker setup when using non-GNU make (JohanBaskovec)
- Fixed actors deletion that didn't cascade to followers
### Security
- Sanitize event title to avoid XSS
## [1.0.0-beta.1] - 2019-10-15
### Added
- Initial release

View File

@ -31,7 +31,7 @@ export default {
},
participantStats: {
approved: 1,
unapproved: 2
notApproved: 2
}
},
actor: {
@ -75,20 +75,20 @@ export default {
</span>
<span class="column is-narrow participant-stats">
<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-->
<!-- v-if="participation.event.options.maximumAttendeeCapacity > 0"-->
<!-- 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>-->
</span>
<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
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 } })">
{{ $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>
</span>
</span>

View File

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

View File

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

View File

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

View File

@ -94,10 +94,13 @@ export enum CommentModeration {
}
export interface IEventParticipantStats {
approved: number;
unapproved: number;
notApproved: number;
rejected: number;
participants: number;
participant: number;
creator: number;
moderator: number;
administrator: number;
going: number;
}
export interface IEvent {
@ -192,7 +195,7 @@ export class EventModel implements IEvent {
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[] = [];
relatedEvents: IEvent[] = [];

View File

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

View File

@ -4,7 +4,7 @@
<b-tab-item>
<template slot="header">
<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>
<section v-if="participantsAndCreators.length > 0">
@ -22,10 +22,10 @@
</section>
</template>
</b-tab-item>
<b-tab-item :disabled="participantStats.unapproved === 0">
<b-tab-item :disabled="participantStats.notApproved === 0">
<template slot="header">
<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>
<section v-if="queue.length > 0">
@ -182,7 +182,7 @@ export default class Participants extends Vue {
@Watch('participantStats', { deep: true })
watchParticipantStats(stats: IEventParticipantStats) {
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;
}
}
@ -223,9 +223,9 @@ export default class Participants extends Vue {
if (data) {
this.queue = this.queue.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) {
this.event.participantStats.unapproved -= 1;
this.event.participantStats.notApproved -= 1;
}
if (participant.role === ParticipantRole.REJECTED) {
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.event.participantStats.rejected += 1;
if (participant.role === ParticipantRole.PARTICIPANT) {
this.event.participantStats.participants -= 1;
this.event.participantStats.approved -= 1;
this.event.participantStats.participant -= 1;
this.event.participantStats.going -= 1;
}
if (participant.role === ParticipantRole.NOT_APPROVED) {
this.event.participantStats.unapproved -= 1;
this.event.participantStats.notApproved -= 1;
}
participant.role = ParticipantRole.REJECTED;
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
alias MobilizonWeb.API
alias MobilizonWeb.API.Comments
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
require Logger
@shortdoc "Toot to an user"
def run([from, content]) do
def run([from, text]) do
Mix.Task.run("app.start")
case API.Comments.create_comment(from, content) do
{:ok, _, _} ->
Mix.shell().info("Tooted")
with {:local_actor, %Actor{} = actor} <- {:local_actor, Actors.get_local_actor_by_name(from)},
{:ok, _, _} <- Comments.create_comment(%{actor: actor, text: text}) do
Mix.shell().info("Tooted")
else
{:local_actor, _, _} ->
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.Reports.{Note, Report}
alias Mobilizon.Users.User
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@ -46,6 +47,7 @@ defmodule Mobilizon.Actors.Actor do
created_reports: [Report.t()],
subject_reports: [Report.t()],
report_notes: [Note.t()],
mentions: [Mention.t()],
memberships: [t]
}
@ -139,6 +141,7 @@ defmodule Mobilizon.Actors.Actor do
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
has_many(:mentions, Mention)
many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps()

View File

@ -88,7 +88,10 @@ defmodule Mobilizon.Actors do
"""
@spec get_actor_by_url(String.t(), boolean) ::
{: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
nil ->
{:error, :actor_not_found}

View File

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

View File

@ -8,7 +8,8 @@ defmodule Mobilizon.Events.Comment do
import Ecto.Changeset
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.Router.Helpers, as: Routes
@ -22,6 +23,8 @@ defmodule Mobilizon.Events.Comment do
actor: Actor.t(),
attributed_to: Actor.t(),
event: Event.t(),
tags: [Tag.t()],
mentions: [Mention.t()],
in_reply_to_comment: t,
origin_comment: t
}
@ -42,6 +45,8 @@ defmodule Mobilizon.Events.Comment do
belongs_to(:event, Event, foreign_key: :event_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)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
timestamps(type: :utc_datetime)
end
@ -57,16 +62,45 @@ defmodule Mobilizon.Events.Comment do
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do
uuid = attrs["uuid"] || Ecto.UUID.generate()
url = attrs["url"] || generate_url(uuid)
uuid = Map.get(attrs, :uuid) || Ecto.UUID.generate()
url = Map.get(attrs, :url) || generate_url(uuid)
comment
|> cast(attrs, @attrs)
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> put_tags(attrs)
|> put_mentions(attrs)
|> validate_required(@required_attrs)
end
@spec generate_url(String.t()) :: String.t()
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

View File

@ -6,22 +6,31 @@ defmodule Mobilizon.Events.Event do
use Ecto.Schema
import Ecto.Changeset
alias Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Addresses
alias Mobilizon.Events.{
EventOptions,
EventStatus,
EventVisibility,
JoinOptions,
EventParticipantStats,
Participant,
Session,
Tag,
Track
}
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@type t :: %__MODULE__{
url: String.t(),
@ -47,30 +56,15 @@ defmodule Mobilizon.Events.Event do
picture: Picture.t(),
tracks: [Track.t()],
sessions: [Session.t()],
mentions: [Mention.t()],
tags: [Tag.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 [
: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,
:description,
:ends_on,
@ -85,7 +79,9 @@ defmodule Mobilizon.Events.Event do
:picture_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
field(:url, :string)
@ -105,13 +101,15 @@ defmodule Mobilizon.Events.Event do
field(:phone_address, :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(:attributed_to, Actor, foreign_key: :attributed_to_id)
belongs_to(:physical_address, Address)
belongs_to(:picture, Picture)
belongs_to(:physical_address, Address, on_replace: :update)
belongs_to(:picture, Picture, on_replace: :update)
has_many(:tracks, Track)
has_many(:sessions, Session)
has_many(:mentions, Mention)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
@ -119,28 +117,41 @@ defmodule Mobilizon.Events.Event do
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
@spec changeset(t, map) :: Changeset.t()
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
|> cast(attrs, @attrs)
|> cast_embed(:options)
|> common_changeset(attrs)
|> put_creator_if_published(:create)
|> validate_required(@required_attrs)
|> validate_lengths()
end
@doc false
@spec update_changeset(t, map) :: Ecto.Changeset.t()
@spec update_changeset(t, map) :: Changeset.t()
def update_changeset(%__MODULE__{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, @update_attrs)
|> cast_embed(:options)
|> put_tags(attrs)
|> cast(attrs, @update_attrs)
|> common_changeset(attrs)
|> put_creator_if_published(:update)
|> validate_required(@update_required_attrs)
|> validate_lengths()
end
@spec validate_lengths(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_lengths(%Ecto.Changeset{} = changeset) do
@spec common_changeset(Changeset.t(), map) :: Changeset.t()
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
|> validate_length(:title, min: 3, max: 200)
|> 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}
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
@spec put_tags(Changeset.t(), map) :: Changeset.t()
defp put_tags(%Changeset{} = changeset, %{tags: tags}),
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

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 EctoEnum
alias Ecto.{Multi, Changeset}
import Mobilizon.Storage.Ecto
@ -17,6 +18,7 @@ defmodule Mobilizon.Events do
alias Mobilizon.Events.{
Comment,
Event,
EventParticipantStats,
FeedToken,
Participant,
Session,
@ -82,6 +84,8 @@ defmodule Mobilizon.Events do
@event_preloads [
:organizer_actor,
:attributed_to,
:mentions,
:sessions,
:tracks,
:tags,
@ -90,7 +94,7 @@ defmodule Mobilizon.Events do
:picture
]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment, :tags, :mentions]
@doc """
Gets a single event.
@ -235,81 +239,103 @@ defmodule Mobilizon.Events do
|> Repo.one()
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 """
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
with {:ok, %Event{draft: false} = event} <- do_create_event(attrs),
{:ok, %Participant{} = _participant} <-
create_participant(%{
actor_id: event.organizer_actor_id,
role: :creator,
event_id: event.id
}) do
with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs),
%Event{} = event <- Repo.preload(event, @event_preloads) do
Task.start(fn -> Search.insert_search_event(event) end)
{:ok, event}
else
# We don't create a creator participant if the event is a draft
{:ok, %Event{draft: true} = event} -> {:ok, event}
err -> err
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
with {:ok, %Event{} = event} <-
%Event{}
|> Event.changeset(attrs)
|> Ecto.Changeset.put_assoc(:tags, Map.get(attrs, "tags", []))
|> Repo.insert(),
%Event{} = event <-
Repo.preload(event, [:tags, :organizer_actor, :physical_address, :picture]) do
{:ok, event}
end
Multi.new()
|> Multi.insert(:insert, Event.changeset(%Event{}, attrs))
|> Multi.run(:write, fn _repo, %{insert: %Event{draft: draft} = event} ->
with {:is_draft, false} <- {:is_draft, draft},
{:ok, %Participant{} = participant} <-
create_participant(
%{
event_id: event.id,
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
@doc """
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()}
def update_event(
%Event{draft: old_draft_status, id: event_id, organizer_actor_id: organizer_actor_id} =
old_event,
attrs
) do
with %Ecto.Changeset{changes: changes} = changeset <-
old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do
with {:ok, %Event{draft: new_draft_status} = new_event} <- Repo.update(changeset) do
# If the event is no longer a draft
if old_draft_status == true && new_draft_status == false do
{:ok, %Participant{} = _participant} =
create_participant(%{
event_id: event_id,
role: :creator,
actor_id: organizer_actor_id
})
end
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def update_event(%Event{} = old_event, attrs) do
with %Changeset{changes: changes} = changeset <-
Event.update_changeset(Repo.preload(old_event, :tags), attrs),
{:ok, %{update: %Event{} = new_event}} <-
Multi.new()
|> Multi.update(
:update,
changeset
)
|> Multi.run(:write, fn _repo, %{update: %Event{draft: draft} = event} ->
with {:is_draft, false} <- {:is_draft, draft},
{:ok, %Participant{} = participant} <-
create_participant(
%{
event_id: event.id,
role: :creator,
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(
old_event,
new_event,
changes
)
Task.start(fn -> Search.update_search_event(new_event) end)
Task.start(fn -> Search.update_search_event(new_event) end)
{:ok, new_event}
end
{:ok, Repo.preload(new_event, @event_preloads)}
end
end
@doc """
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)
@doc """
@ -450,7 +476,7 @@ defmodule Mobilizon.Events do
@doc """
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
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
@ -461,10 +487,24 @@ defmodule Mobilizon.Events do
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 """
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
%Tag{}
|> Tag.changeset(attrs)
@ -474,7 +514,7 @@ defmodule Mobilizon.Events do
@doc """
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
tag
|> Tag.changeset(attrs)
@ -484,7 +524,7 @@ defmodule Mobilizon.Events do
@doc """
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)
@doc """
@ -524,7 +564,7 @@ defmodule Mobilizon.Events do
@doc """
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
%TagRelation{}
|> TagRelation.changeset(attrs)
@ -538,7 +578,7 @@ defmodule Mobilizon.Events do
Removes a tag relation.
"""
@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
Repo.delete(tag_relation)
end
@ -763,7 +803,7 @@ defmodule Mobilizon.Events do
end
@doc """
Counts participant participants.
Counts participant participants (participants with no extra role)
"""
@spec count_participant_participants(integer | String.t()) :: integer
def count_participant_participants(event_id) do
@ -773,17 +813,6 @@ defmodule Mobilizon.Events do
|> Repo.aggregate(:count, :id)
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 """
Counts rejected participants.
"""
@ -805,12 +834,40 @@ defmodule Mobilizon.Events do
@doc """
Creates a participant.
"""
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
def create_participant(attrs \\ %{}) do
with {:ok, %Participant{} = participant} <-
%Participant{}
|> Participant.changeset(attrs)
|> Repo.insert() do
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Changeset.t()}
def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
with {:ok, %{participant: %Participant{} = participant}} <-
Multi.new()
|> Multi.insert(:participant, Participant.changeset(%Participant{}, attrs))
|> 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])}
end
end
@ -819,7 +876,7 @@ defmodule Mobilizon.Events do
Updates a participant.
"""
@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
participant
|> Participant.changeset(attrs)
@ -830,7 +887,7 @@ defmodule Mobilizon.Events do
Deletes a participant.
"""
@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)
@doc """
@ -843,7 +900,7 @@ defmodule Mobilizon.Events do
@doc """
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
%Session{}
|> Session.changeset(attrs)
@ -853,7 +910,7 @@ defmodule Mobilizon.Events do
@doc """
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
session
|> Session.changeset(attrs)
@ -863,7 +920,7 @@ defmodule Mobilizon.Events do
@doc """
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)
@doc """
@ -892,7 +949,7 @@ defmodule Mobilizon.Events do
@doc """
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
%Track{}
|> Track.changeset(attrs)
@ -902,7 +959,7 @@ defmodule Mobilizon.Events do
@doc """
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
track
|> Track.changeset(attrs)
@ -912,7 +969,7 @@ defmodule Mobilizon.Events do
@doc """
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)
@doc """
@ -931,6 +988,13 @@ defmodule Mobilizon.Events do
|> Repo.all()
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 """
Gets a single comment.
Raises `Ecto.NoResultsError` if the comment does not exist.
@ -994,10 +1058,17 @@ defmodule Mobilizon.Events do
|> Repo.preload(@comment_preloads)
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 """
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
with {:ok, %Comment{} = comment} <-
%Comment{}
@ -1011,7 +1082,7 @@ defmodule Mobilizon.Events do
@doc """
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
comment
|> Comment.changeset(attrs)
@ -1021,7 +1092,7 @@ defmodule Mobilizon.Events do
@doc """
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)
@doc """
@ -1097,7 +1168,7 @@ defmodule Mobilizon.Events do
@doc """
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
attrs = Map.put(attrs, "token", Ecto.UUID.generate())
@ -1110,7 +1181,7 @@ defmodule Mobilizon.Events do
Updates a feed token.
"""
@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
feed_token
|> FeedToken.changeset(attrs)
@ -1120,7 +1191,7 @@ defmodule Mobilizon.Events do
@doc """
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)
@doc """
@ -1354,7 +1425,7 @@ defmodule Mobilizon.Events do
end
@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)
end
@ -1385,17 +1456,10 @@ defmodule Mobilizon.Events do
end
defp public_comments_for_actor_query(actor_id) do
from(
c in Comment,
where: c.actor_id == ^actor_id and c.visibility in ^@public_visibility,
order_by: [desc: :id],
preload: [
:actor,
:in_reply_to_comment,
:origin_comment,
:event
]
)
Comment
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
|> order_by([c], desc: :id)
|> preload_for_comment()
end
@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()
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
@spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_participant_role(query) do
from(p in query, where: p.role == ^: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)
filter_role(query, :participant)
end
@spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_rejected_role(query) do
from(p in query, where: p.role == ^:rejected)
filter_role(query, :rejected)
end
@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)
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),
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 """
API for Comments.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Activity
@doc """
Create a comment
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()
def create_comment(
from_username,
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)
def create_comment(args) do
ActivityPub.create(:comment, args, true)
end
end

View File

@ -3,37 +3,24 @@ defmodule MobilizonWeb.API.Events do
API for Events.
"""
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Utils
@doc """
Create an event
"""
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
def create_event(%{organizer_actor: organizer_actor} = args) do
with args <- prepare_args(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
) 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
})
def create_event(args) do
with organizer_actor <- Map.get(args, :organizer_actor),
args <-
Map.update(args, :picture, nil, fn picture ->
process_picture(picture, organizer_actor)
end) do
# For now we don't federate drafts but it will be needed if we want to edit them as groups
ActivityPub.create(:event, args, args.draft == false)
end
end
@ -41,65 +28,13 @@ defmodule MobilizonWeb.API.Events do
Update an event
"""
@spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
def update_event(
%{
organizer_actor: organizer_actor
} = args,
%Event{} = event
) do
with args <- Map.put(args, :tags, Map.get(args, :tags, [])),
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)
}
}
def update_event(args, %Event{} = event) do
with organizer_actor <- Map.get(args, :organizer_actor),
args <-
Map.update(args, :picture, nil, fn picture ->
process_picture(picture, organizer_actor)
end) do
ActivityPub.update(:event, event, args, Map.get(args, :draft, false) == false)
end
end
@ -111,4 +46,15 @@ defmodule MobilizonWeb.API.Events do
def delete_event(%Event{} = event, federate \\ true) do
ActivityPub.delete(event, federate)
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

View File

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

View File

@ -6,38 +6,19 @@ defmodule MobilizonWeb.API.Groups do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias Mobilizon.Users.User
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Activity
@doc """
Create a group
"""
@spec create_group(User.t(), map()) :: {:ok, Activity.t(), Group.t()} | any()
def create_group(
user,
%{
preferred_username: title,
summary: summary,
creator_actor_id: creator_actor_id,
avatar: _avatar,
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
})
@spec create_group(map()) :: {:ok, Activity.t(), Actor.t()} | any()
def create_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
{:existing_group, nil} <-
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
{:ok, %Activity{} = activity, %Actor{} = group} <- ActivityPub.create(:group, args, true) do
{:ok, activity, group}
else
{:existing_group, _} ->
{:error, "A group with this name already exists"}

View File

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

View File

@ -3,89 +3,9 @@ defmodule MobilizonWeb.API.Utils do
Utils for API.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Config
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 """
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"}
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

View File

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

View File

@ -7,11 +7,8 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Events.{Event, Participant, EventParticipantStats}
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
@ -96,14 +93,8 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok, []}
end
def stats_participants_for_event(%Event{id: id}, _args, _resolution) do
{:ok,
%{
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)
}}
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
{:ok, stats.participant + stats.moderator + stats.administrator + stats.creator}
end
@doc """
@ -277,8 +268,6 @@ defmodule MobilizonWeb.Resolvers.Event do
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
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} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
@ -309,8 +298,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, event.organizer_actor_id),
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} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
@ -327,47 +314,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to update an event"}
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 """
Delete an event
"""

View File

@ -6,7 +6,6 @@ defmodule MobilizonWeb.Resolvers.Group do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
alias MobilizonWeb.API
@ -47,23 +46,18 @@ defmodule MobilizonWeb.Resolvers.Group do
args,
%{context: %{current_user: user}}
) do
with {
:ok,
%Activity{data: %{"object" => %{"type" => "Group"} = _object}},
%Actor{} = group
} <-
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
with creator_actor_id <- Map.get(args, :creator_actor_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
args <- Map.put(args, :creator_actor, actor),
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{: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

View File

@ -155,7 +155,7 @@ defmodule MobilizonWeb.Resolvers.Person do
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
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
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end

View File

@ -51,7 +51,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
%{context: %{current_user: user}}
) do
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),
args <-
args

View File

@ -35,7 +35,7 @@ defmodule MobilizonWeb.Schema.CommentType do
@desc "Create a comment"
field :create_comment, type: :comment do
arg(:text, non_null(:string))
arg(:actor_username, non_null(:string))
arg(:actor_id, non_null(:id))
resolve(&Comment.create_comment/3)
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(: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
arg(:page, :integer, default_value: 1)
@ -112,13 +112,21 @@ defmodule MobilizonWeb.Schema.EventType do
end
object :participant_stats do
field(:approved, :integer, description: "The number of approved participants")
field(:unapproved, :integer, description: "The number of unapproved participants")
field(:going, :integer,
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(:participants, :integer,
field(:participant, :integer,
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
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,
%{
"type" => opts.activity_type || get_type(upload.content_type),
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"size" => upload.size,
"name" => Map.get(opts, :description) || upload.name
name: Map.get(opts, :description) || upload.name,
url: url_from_spec(upload, opts.base_url, url_spec),
content_type: upload.content_type,
size: upload.size
}}
else
{:error, error} ->
@ -166,16 +160,6 @@ defmodule MobilizonWeb.Upload do
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
path =
URI.encode(path, &char_unescaped?/1) <>

View File

@ -8,7 +8,8 @@ defmodule Mobilizon.Service.ActivityPub do
# 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.{Actor, Follower}
@ -16,17 +17,12 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Relay, Transmogrifier}
alias Mobilizon.Service.{Federator, WebFinger}
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
@doc """
Get recipients for an activity or object
"""
@spec get_recipients(map()) :: list()
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
@doc """
Wraps an object into an activity
"""
@ -106,8 +102,8 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """
Getting an actor from url, eventually creating it
"""
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do
@spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_actor_by_url(url, preload \\ false) do
case Actors.get_actor_by_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
@ -126,27 +122,29 @@ defmodule Mobilizon.Service.ActivityPub do
end
@doc """
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]
Create an activity of type `Create`
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, object: object},
additional
),
{:ok, activity} <- create_activity(create_data, local),
{:ok, object} <- insert_full_object(create_data),
* Creates the object, which returns AS data
* Wraps ActivityStreams data into a `Create` activity
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
* Federates (asynchronously) the activity
* Returns the activity
"""
@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, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object}
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
@ -155,21 +153,52 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def accept(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
@doc """
Create an activity of type `Update`
with data <- %{
"to" => to,
"type" => "Accept",
"actor" => actor,
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
* Updates the object, which returns AS data
* Wraps ActivityStreams data into a `Update` activity
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
* Federates (asynchronously) the activity
* Returns the activity
"""
@spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
def update(type, old_entity, args, local \\ false, additional \\ %{}) do
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, 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
@ -191,25 +220,6 @@ defmodule Mobilizon.Service.ActivityPub do
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.
# def like(
# %Actor{url: url} = actor,
@ -290,15 +300,12 @@ defmodule Mobilizon.Service.ActivityPub do
Make an actor follow another
"""
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),
activity_follow_id <-
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
follower_as_data <- Convertible.model_to_as(follower),
{:ok, activity} <- create_activity(follower_as_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, follower}
else
{:error, err, msg} when err in [:already_following, :suspended] ->
{:error, msg}
@ -310,16 +317,11 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
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
data <-
make_follow_data(
followed,
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity} <- create_activity(data, local),
{:ok, _object} <- insert_full_object(data),
follow_as_data <-
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
{:ok, follow_activity} <- create_activity(follow_as_data, local),
activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
@ -346,7 +348,7 @@ defmodule Mobilizon.Service.ActivityPub do
"id" => url <> "/delete"
}
with {:ok, _} <- Events.delete_event(event),
with {:ok, %Event{} = event} <- Events.delete_event(event),
{:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, event}
@ -362,11 +364,10 @@ defmodule Mobilizon.Service.ActivityPub do
"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, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, comment}
end
end
@ -379,11 +380,10 @@ defmodule Mobilizon.Service.ActivityPub do
"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, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, actor}
end
end
@ -434,9 +434,9 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _object} <- insert_full_object(join_data),
:ok <- maybe_federate(activity) do
if role === :participant do
accept(
%{to: [actor.url], actor: event.organizer_actor.url, object: join_data["id"]},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
accept_join(
participant,
%{}
)
end
@ -720,9 +720,233 @@ defmodule Mobilizon.Service.ActivityPub do
}
end
# # Whether the Public audience is in the activity's audience
# defp is_public?(activity) do
# "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
# (activity.data["cc"] || []))
# end
# Get recipients for an activity or object
@spec get_recipients(map()) :: list()
defp get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
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

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"]
}
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}
{:ok,
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}}
end
@doc """

View File

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

View File

@ -6,15 +6,17 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
internal one, and back.
"""
alias Mobilizon.{Actors, Addresses, Events, Media}
alias Mobilizon.{Addresses, Media}
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.{EventOptions, Tag}
alias Mobilizon.Events.EventOptions
alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger
@ -30,16 +32,16 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
Converts an AP object data to our internal data structure.
"""
@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
Logger.debug("event as_to_model_data")
Logger.debug(inspect(object))
with {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, Actors.get_actor_by_url(object["actor"])},
{:actor, ActivityPub.get_or_fetch_actor_by_url(object["actor"])},
{:address, address_id} <-
{: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)},
{:options, options} <- {:options, get_options(object)} do
picture_id =
@ -58,26 +60,27 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
end
entity = %{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"picture_id" => picture_id,
"begins_on" => object["startTime"],
"ends_on" => object["endTime"],
"category" => object["category"],
"visibility" => visibility,
"join_options" => object["joinOptions"],
"status" => object["status"],
"online_address" => object["onlineAddress"],
"phone_address" => object["phoneAddress"],
"draft" => object["draft"] || false,
"url" => object["id"],
"uuid" => object["uuid"],
"tags" => tags,
"physical_address_id" => address_id
title: object["name"],
description: object["content"],
organizer_actor_id: actor_id,
picture_id: picture_id,
begins_on: object["startTime"],
ends_on: object["endTime"],
category: object["category"],
visibility: visibility,
join_options: Map.get(object, "joinOptions", "free"),
options: options,
status: object["status"],
online_address: object["onlineAddress"],
phone_address: object["phoneAddress"],
draft: object["draft"] || false,
url: object["id"],
uuid: object["uuid"],
tags: tags,
physical_address_id: address_id
}
{:ok, Map.put(entity, "options", options)}
{:ok, entity}
else
error ->
{:error, error}
@ -111,7 +114,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"startTime" => event.begins_on |> date_to_string(),
"joinOptions" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"tag" => event.tags |> ConverterUtils.build_tags(),
"draft" => event.draft,
"id" => event.url,
"url" => event.url
@ -181,32 +184,6 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
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"
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
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
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@ -39,7 +39,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def unfollow(target_instance) do
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
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
@ -52,7 +52,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def accept(target_instance) do
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}
end
@ -60,7 +60,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
# def reject(target_instance) do
# 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}
# end

View File

@ -13,9 +13,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils, Visibility}
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Utils}
alias MobilizonWeb.Email.Participation
import Mobilizon.Service.ActivityPub.Utils
require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do
@ -138,59 +140,65 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
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")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
params = %{
to: data["to"],
object: object |> fix_object,
actor: actor,
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
with {:ok, object_data} <-
object |> fix_object() |> Converter.Comment.as_to_model_data(),
{:existing_comment, {:error, :comment_not_found}} <-
{:existing_comment, Events.get_comment_from_url_with_preload(object_data.url)},
{:ok, %Activity{} = activity, %Comment{} = comment} <-
ActivityPub.create(:comment, object_data, false) do
{:ok, activity, comment}
else
{:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, comment}
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")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
params = %{
to: data["to"],
object: object |> fix_object,
actor: actor,
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
with {:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do
{:ok, activity, event}
else
{:existing_event, %Event{} = event} -> {:ok, nil, event}
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_by_url(follower),
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_actor_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
{:ok, activity, object}
else
@ -209,8 +217,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, %Activity{} = activity, object}} <-
{:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) ||
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
) do
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,
do_handle_incoming_reject_following(rejected_object, actor) ||
@ -278,13 +286,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end
# #
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => _id} = data
) do
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),
public <- Visibility.is_public?(data),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
:ok <- Logger.debug("Handling contained object"),
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}
else
e ->
@ -293,21 +308,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
data
)
def handle_incoming(%{
"type" => "Update",
"object" => %{"type" => object_type} = object,
"actor" => _actor_id
})
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
case Actors.get_actor_by_url(object["id"]) do
{:ok, %Actor{url: actor_url}} ->
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_url
})
with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
{:ok, object_data} <-
object |> fix_object() |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
ActivityPub.update(:actor, old_actor, object_data, false) do
{:ok, activity, new_actor}
else
e ->
Logger.debug(inspect(e))
:error
@ -315,24 +328,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} =
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
_update
) do
with {:ok, %{"actor" => existing_organizer_actor_url} = existing_event_data} <-
fetch_obj_helper_as_activity_streams(object),
object <- Map.merge(existing_event_data, object),
{:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(),
true <- Utils.get_url(existing_organizer_actor_url) == actor_url do
ActivityPub.update(%{
local: false,
to: object["to"] || [],
cc: object["cc"] || [],
object: object,
actor: actor_url
})
with %Event{} = old_event <-
Events.get_event_by_url(object["id"]),
{:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
{:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(:event, old_event, object_data, false) do
{:ok, activity, new_event}
else
e ->
Logger.debug(inspect(e))
Logger.error(inspect(e))
:error
end
end
@ -350,7 +358,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
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, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
@ -454,7 +462,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# } = data
# ) do
# 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, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity}
@ -472,23 +480,16 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Handle incoming `Accept` activities wrapping a `Follow` activity
"""
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
with {:follow,
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: actor.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
:follow,
follow,
%{approved: true},
false
) do
{:ok, activity, follow}
else
{:follow, _} ->
@ -546,26 +547,18 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
with {:join_event, {:ok, %Participant{role: :not_approved, event: event} = participant}} <-
{:join_event, get_participant(join_object)},
# 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
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <-
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept(
%{
to: [actor.url],
actor: actor_accepting.url,
object: join_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{join_id}"
:join,
participant,
%{role: :participant},
false
),
{:ok, %Participant{role: :participant}} <-
Events.update_participant(participant, %{"role" => :participant}),
:ok <-
Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
@ -684,7 +677,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def prepare_object(object) do
object
# |> set_sensitive
|> add_hashtags
# |> add_hashtags
|> add_mention_tags
# |> add_emoji_tags
|> add_attributed_to
@ -781,6 +774,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def add_mention_tags(object) do
Logger.debug("add mention tags")
Logger.debug(inspect(object))
recipients =
(object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"]
@ -795,7 +791,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end)
|> Enum.filter(& &1)
|> 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)
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()
def fetch_obj_helper(object) do
Logger.debug("fetch_obj_helper")
Logger.debug("Fetching object #{inspect(object)}")
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
@ -867,6 +868,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
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
{:ok, Convertible.model_to_as(object)}
end

View File

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

View File

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

View File

@ -52,7 +52,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
@spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
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}
else

View File

@ -1,12 +1,5 @@
defmodule Mobilizon.Storage.Repo.Migrations.AddEagerMaterializedViewForSearchingEvents do
use Ecto.Migration
import Ecto.Query
alias Mobilizon.Storage.Repo
alias Mobilizon.Service.Search
alias Mobilizon.Events.Event
require Logger
def up do
create table(:event_search, primary_key: false) do
@ -28,35 +21,6 @@ defmodule Mobilizon.Storage.Repo.Migrations.AddEagerMaterializedViewForSearching
# to support updating CONCURRENTLY
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
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
# 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 {
query: RootQueryType
@ -690,17 +690,26 @@ enum ParticipantRoleEnum {
}
type ParticipantStats {
"""The number of administrators"""
administrator: Int
"""The number of creators"""
creator: Int
"""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)"""
participants: Int
participant: Int
"""The number of rejected participants"""
rejected: Int
"""The number of unapproved participants"""
unapproved: Int
}
"""
@ -908,7 +917,7 @@ type RootMutationType {
changePassword(newPassword: String!, oldPassword: String!): User
"""Create a comment"""
createComment(actorUsername: String!, text: String!): Comment
createComment(actorId: ID!, text: String!): Comment
"""Create an event"""
createEvent(

View File

@ -28,9 +28,9 @@
"attributedTo": "https://framapiaf.org/users/admin",
"cc": [
"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",
"id": "https://framapiaf.org/users/admin/statuses/99512778738411822",
"inReplyTo": null,
@ -40,8 +40,8 @@
"summary": "cw",
"tag": [
{
"href": "http://localtesting.pleroma.lol/users/lain",
"name": "@lain@localtesting.pleroma.lol",
"href": "https://framapiaf.org/users/tcit",
"name": "@tcit@framapiaf.org",
"type": "Mention"
},
{

View File

@ -28,9 +28,9 @@
"attributedTo": "https://framapiaf.org/users/admin",
"cc": [
"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",
"id": "https://framapiaf.org/users/admin/statuses/99512778738411822",
"inReplyTo": null,
@ -40,8 +40,8 @@
"summary": "cw",
"tag": [
{
"href": "http://localtesting.pleroma.lol/users/lain",
"name": "@lain@localtesting.pleroma.lol",
"href": "https://framapiaf.org/users/tcit",
"name": "@tcit@framapiaf.org",
"type": "Mention"
}
],

View File

@ -14,7 +14,7 @@
"actor": "https://event1.tcit.fr/@tcit",
"cc": [
"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",
"object": {
@ -23,9 +23,9 @@
"startTime": "2018-02-12T14:08:20Z",
"cc": [
"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",
"id": "https://event1.tcit.fr/@tcit/events/109ccdfd-ee3e-46e1-a877-6c228763df0c",
"inReplyTo": null,
@ -46,8 +46,8 @@
"published": "2018-02-12T14:08:20Z",
"tag": [
{
"href": "http://localtesting.pleroma.lol/users/lain",
"name": "@lain@localtesting.pleroma.lol",
"href": "https://framapiaf.org/users/tcit",
"name": "@tcit@framapiaf.org",
"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,
domain: domain,
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"
@ -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
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(
"#{actor.preferred_username}@#{actor.domain}"
).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",
%{actor: %Actor{id: actor_id}} 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} =
Actors.build_actors_by_username_or_name_page("tcit", [:Person])
@ -253,12 +254,11 @@ defmodule Mobilizon.ActorsTest do
}
{:ok, data} = MobilizonWeb.Upload.store(file)
url = hd(data["url"])["href"]
assert {:ok, actor} =
Actors.update_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

View File

@ -115,7 +115,7 @@ defmodule Mobilizon.EventsTest do
end
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
test "update_event/2 with valid data updates the event", %{event: event} do
@ -128,7 +128,7 @@ defmodule Mobilizon.EventsTest do
end
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
end
@ -345,7 +345,8 @@ defmodule Mobilizon.EventsTest do
end
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
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.Events
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.HTTPSignatures.Signature
alias MobilizonWeb.ActivityPub.ActorView
@activity_pub_public_audience "https://www.w3.org/ns/activitystreams#Public"
setup_all do
HTTPoison.start()
@ -53,7 +51,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "returns an actor from url" do
use_cassette "activity_pub/fetch_framapiaf.org_users_tcit" do
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
@ -165,28 +163,15 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
test "it creates an update activity with the new actor data" do
actor = insert(:actor)
actor_data = ActorView.render("actor.json", %{actor: actor})
actor_data = Map.put(actor_data, "summary", @updated_actor_summary)
actor_data = %{summary: @updated_actor_summary}
{:ok, update, updated_actor} =
ActivityPub.update(%{
actor: actor_data["url"],
to: [actor.url <> "/followers"],
cc: [],
object: actor_data
})
{:ok, update, _} = ActivityPub.update(:actor, actor, actor_data, false)
assert update.data["actor"] == actor.url
assert update.data["to"] == [actor.url <> "/followers"]
assert update.data["object"]["id"] == actor_data["id"]
assert update.data["object"]["type"] == actor_data["type"]
assert update.data["to"] == [@activity_pub_public_audience]
assert update.data["object"]["id"] == actor.url
assert update.data["object"]["type"] == "Person"
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
@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
actor = insert(:actor)
event = insert(:event, organizer_actor: actor)
event_data = Converter.Event.model_to_as(event)
event_data = Map.put(event_data, "startTime", @updated_start_time)
event_data = %{begins_on: @updated_start_time}
{:ok, update, updated_event} =
ActivityPub.update(%{
actor: actor.url,
to: [actor.url <> "/followers"],
cc: [],
object: event_data
})
{:ok, update, _} = ActivityPub.update(:event, event, event_data)
assert update.data["actor"] == actor.url
assert update.data["to"] == [actor.url <> "/followers"]
assert update.data["object"]["id"] == event_data["id"]
assert update.data["object"]["type"] == event_data["type"]
assert update.data["object"]["startTime"] == @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
assert update.data["to"] == [@activity_pub_public_audience]
assert update.data["object"]["id"] == event.url
assert update.data["object"]["type"] == "Event"
assert update.data["object"]["startTime"] == DateTime.to_iso8601(@updated_start_time)
end
end
end

View File

@ -15,7 +15,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.ActorTest do
describe "AS to Actor" do
test "valid as data to model" do
actor =
{:ok, actor} =
ActorConverter.as_to_model_data(%{
"type" => "Person",
"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["cc"] == [
"https://framapiaf.org/users/admin/followers",
"http://mobilizon.com/@tcit"
]
# assert data["cc"] == [
# "https://framapiaf.org/users/admin/followers",
# "http://mobilizon.com/@tcit"
# ]
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["cc"] == [
"https://framapiaf.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
# assert object["cc"] == [
# "https://framapiaf.org/users/admin/followers",
# "http://localtesting.pleroma.lol/users/lain"
# ]
assert object["actor"] == "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"])
end
@ -153,6 +151,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
assert Enum.at(data["object"]["tag"], 0)["name"] == "@tcit@framapiaf.org"
assert Enum.at(data["object"]["tag"], 1)["name"] == "#moo"
end
@ -347,9 +346,9 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("actor", data["actor"])
|> 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.summary == "<p>Some bio</p>"
@ -406,7 +405,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it works for incoming deletes" do
%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 =
File.read!("test/fixtures/mastodon-delete.json")
@ -622,8 +621,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("object", follow_activity.data["id"])
{:ok, activity, _} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
assert activity.data["object"] =~ "/follow/"
assert activity.data["object"]["id"] == follow_activity.data["id"]
assert activity.data["object"]["id"] =~ "/follow/"
assert activity.data["id"] =~ "/accept/follow/"
{:ok, follower} = Actors.get_actor_by_url(follower.url)
@ -756,8 +755,8 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|> Map.put("object", participation.url)
{:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
assert accept_activity.data["object"] == join_activity.data["id"]
assert accept_activity.data["object"] =~ "/join/"
assert accept_activity.data["object"]["id"] == join_activity.data["id"]
assert accept_activity.data["object"]["id"] =~ "/join/"
assert accept_activity.data["id"] =~ "/accept/join/"
# We don't accept already accepted Accept activities
@ -847,10 +846,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
other_actor = insert(:actor)
{:ok, activity, _} =
API.Comments.create_comment(
actor.preferred_username,
"hey, @#{other_actor.preferred_username}, how are ya? #2hu"
)
API.Comments.create_comment(%{
actor_id: actor.id,
text: "hey, @#{other_actor.preferred_username}, how are ya? #2hu"
})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
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
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)
@ -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
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)
@ -903,7 +902,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it strips internal hashtag data" do
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 = %{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/2hu",
@ -919,7 +918,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
test "it strips internal fields" do
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)

View File

@ -17,12 +17,21 @@ defmodule Mobilizon.Service.ActivityPub.UtilsTest do
describe "make" do
test "comment data from struct" do
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 %{
"type" => "Note",
"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,
"uuid" => 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)
[%{"href" => attachment_url} | _] = data["url"]
[attachment_url: attachment_url]
[attachment_url: data.url]
end
setup_all :upload_file

View File

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

View File

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

View File

@ -2,7 +2,6 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
use MobilizonWeb.ConnCase
alias MobilizonWeb.AbsintheHelpers
import Mobilizon.Factory
require Logger
@non_existent_username "nonexistent"
@new_group_params %{groupname: "new group"}
@ -36,7 +35,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
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
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}") {
uuid,
participantStats {
approved,
unapproved,
going,
notApproved,
rejected
}
}
@ -574,8 +574,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> 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"]["participantStats"]["approved"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["unapproved"] == 0
assert json_response(res, 200)["data"]["event"]["participantStats"]["going"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["notApproved"] == 0
assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 0
moderator = insert(:actor)
@ -586,18 +586,18 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
actor_id: moderator.id
})
unapproved = insert(:actor)
not_approved = insert(:actor)
Events.create_participant(%{
role: :not_approved,
event_id: event.id,
actor_id: unapproved.id
actor_id: not_approved.id
})
Events.create_participant(%{
role: :rejected,
event_id: event.id,
actor_id: unapproved.id
actor_id: not_approved.id
})
query = """
@ -605,8 +605,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
event(uuid: "#{event.uuid}") {
uuid,
participantStats {
approved,
unapproved,
going,
notApproved,
rejected
}
}
@ -618,8 +618,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> 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"]["participantStats"]["approved"] == 2
assert json_response(res, 200)["data"]["event"]["participantStats"]["unapproved"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["going"] == 2
assert json_response(res, 200)["data"]["event"]["participantStats"]["notApproved"] == 1
assert json_response(res, 200)["data"]["event"]["participantStats"]["rejected"] == 1
end
end

View File

@ -25,9 +25,9 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file)
assert %{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}],
"size" => 13_227,
"type" => "Image"
url: url,
content_type: "image/jpeg",
size: 13_227
} = data
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)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, base_url <> "/media/")
assert String.starts_with?(data.url, base_url <> "/media/")
end
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])
assert List.first(data["url"])["href"] ==
assert data.url ==
MobilizonWeb.Endpoint.url() <>
"/media/590523d60d3831ec92d05cdd871078409d5780903910efec5cd35ab1b0f19d11.jpg"
end
@ -77,7 +75,7 @@ defmodule Mobilizon.UploadTest do
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
assert data.name == "an [image.jpg"
end
test "fixes incorrect content type" do
@ -90,7 +88,7 @@ defmodule Mobilizon.UploadTest do
}
{:ok, data} = Upload.store(file, filters: [MobilizonWeb.Upload.Filter.Dedupe])
assert hd(data["url"])["mediaType"] == "image/jpeg"
assert data.content_type == "image/jpeg"
end
test "adds missing extension" do
@ -103,7 +101,7 @@ defmodule Mobilizon.UploadTest do
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
assert data.name == "an [image.jpg"
end
test "fixes incorrect file extension" do
@ -116,7 +114,7 @@ defmodule Mobilizon.UploadTest do
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
assert data.name == "an [image.jpg"
end
test "don't modify filename of an unknown type" do
@ -129,7 +127,7 @@ defmodule Mobilizon.UploadTest do
}
{:ok, data} = Upload.store(file)
assert data["name"] == "test.txt"
assert data.name == "test.txt"
end
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])
refute data["name"] == "an [image.jpg"
refute data.name == "an [image.jpg"
end
test "escapes invalid characters in url" do
@ -156,9 +154,8 @@ defmodule Mobilizon.UploadTest do
}
{: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
test "escapes reserved uri characters" do
@ -171,9 +168,8 @@ defmodule Mobilizon.UploadTest do
}
{: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"
end
@ -210,9 +206,9 @@ defmodule Mobilizon.UploadTest do
{:ok, data} = Upload.store(file)
assert %{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}],
"size" => 13_227,
"type" => "Image"
url: url,
size: 13_227,
content_type: "image/jpeg"
} = data
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),
inbox_url: Actor.build_url(preferred_username, :inbox),
outbox_url: Actor.build_url(preferred_username, :outbox),
user: nil
user: build(:user)
}
end
@ -100,6 +100,8 @@ defmodule Mobilizon.Factory do
actor: build(:actor),
event: build(:event),
uuid: uuid,
mentions: [],
tags: build_list(3, :tag),
in_reply_to_comment: nil,
url: Routes.page_url(Endpoint, :comment, uuid)
}
@ -121,11 +123,13 @@ defmodule Mobilizon.Factory do
physical_address: build(:address),
visibility: :public,
tags: build_list(3, :tag),
mentions: [],
url: Routes.page_url(Endpoint, :event, uuid),
picture: insert(:picture),
uuid: uuid,
join_options: :free,
options: %{}
options: %{},
participant_stats: %{}
}
end
@ -195,9 +199,10 @@ defmodule Mobilizon.Factory do
{:ok, data} = Upload.store(file)
%{
"url" => [%{"href" => url, "mediaType" => "image/jpeg"}],
"size" => 13_227,
"type" => "Image"
content_type: "image/jpeg",
name: "image.jpg",
url: url,
size: 13_227
} = data
%Mobilizon.Media.File{