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:
commit
1bf25a61cd
64
CHANGELOG.md
Normal file
64
CHANGELOG.md
Normal 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
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
: '';
|
||||
|
||||
|
@ -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[] = [];
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
67
lib/mix/tasks/mobilizon/move_participant_stats.ex
Normal file
67
lib/mix/tasks/mobilizon/move_participant_stats.ex
Normal 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
|
49
lib/mix/tasks/mobilizon/setup_search.ex
Normal file
49
lib/mix/tasks/mobilizon/setup_search.ex
Normal 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
|
@ -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, _, _} ->
|
||||
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")
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
44
lib/mobilizon/events/event_participant_stats.ex
Normal file
44
lib/mobilizon/events/event_participant_stats.ex
Normal 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
|
@ -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,62 +239,85 @@ 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}
|
||||
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.
|
||||
"""
|
||||
@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
|
||||
|
||||
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, 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}")
|
||||
|
||||
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
|
||||
@ -301,15 +328,14 @@ defmodule Mobilizon.Events do
|
||||
|
||||
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)
|
||||
|
||||
|
53
lib/mobilizon/mentions/mention.ex
Normal file
53
lib/mobilizon/mentions/mention.ex
Normal 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
|
@ -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
|
||||
|
@ -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,
|
||||
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
|
||||
local: args.metadata.draft == false
|
||||
})
|
||||
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
|
||||
|
@ -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} ->
|
||||
|
@ -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"}
|
||||
|
@ -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}),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) <>
|
||||
|
@ -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
|
||||
|
98
lib/service/activity_pub/audience.ex
Normal file
98
lib/service/activity_pub/audience.ex
Normal 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
|
@ -37,6 +37,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
|
||||
"url" => object["image"]["url"]
|
||||
}
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"type" => String.to_existing_atom(object["type"]),
|
||||
"preferred_username" => object["preferredUsername"],
|
||||
@ -47,7 +48,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
|
||||
"banner" => banner,
|
||||
"keys" => object["publicKey"]["publicKeyPem"],
|
||||
"manually_approves_followers" => object["manuallyApprovesFollowers"]
|
||||
}
|
||||
}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -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,20 +27,26 @@ 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("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
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))
|
||||
|
||||
data = %{
|
||||
"text" => object["content"],
|
||||
"url" => object["id"],
|
||||
"actor_id" => actor_id,
|
||||
"in_reply_to_comment_id" => nil,
|
||||
"event_id" => nil,
|
||||
"uuid" => object["uuid"]
|
||||
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
|
||||
}
|
||||
|
||||
# We fetch the parent object
|
||||
@ -54,15 +61,15 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment 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(:event_id, id)
|
||||
|
||||
# Reply to a comment (Comment)
|
||||
{:ok, %CommentModel{id: id} = comment} ->
|
||||
Logger.debug("Parent object is another comment")
|
||||
|
||||
data
|
||||
|> Map.put("in_reply_to_comment_id", id)
|
||||
|> Map.put("origin_comment_id", comment |> CommentModel.get_thread_id())
|
||||
|> 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} ->
|
||||
@ -76,7 +83,11 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
|
||||
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
|
||||
|
@ -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
|
||||
|
36
lib/service/activity_pub/converter/follower.ex
Normal file
36
lib/service/activity_pub/converter/follower.ex
Normal 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
|
100
lib/service/activity_pub/converter/utils.ex
Normal file
100
lib/service/activity_pub/converter/utils.ex
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
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
|
||||
def handle_incoming(%{
|
||||
"type" => "Update",
|
||||
"object" => %{"type" => object_type} = object,
|
||||
"actor" => _actor_id
|
||||
})
|
||||
|
||||
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
|
||||
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
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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{} ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
14
priv/repo/migrations/20191024204726_add_tags_to_comments.exs
Normal file
14
priv/repo/migrations/20191024204726_add_tags_to_comments.exs
Normal 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
|
16
priv/repo/migrations/20191025083642_add_mention_tables.exs
Normal file
16
priv/repo/migrations/20191025083642_add_mention_tables.exs
Normal 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
|
@ -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
|
@ -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(
|
||||
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
8
test/fixtures/mastodon-post-activity.json
vendored
8
test/fixtures/mastodon-post-activity.json
vendored
@ -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"
|
||||
}
|
||||
],
|
||||
|
10
test/fixtures/mobilizon-post-activity.json
vendored
10
test/fixtures/mobilizon-post-activity.json
vendored
@ -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
@ -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
|
||||
|
@ -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", %{
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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/")
|
||||
|
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user