Merge branch 'stable-1.0.x'

This commit is contained in:
Thomas Citharel 2021-02-04 17:20:59 +01:00
commit 48e5ad89e7
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
21 changed files with 1098 additions and 772 deletions

View file

@ -4,6 +4,18 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.0.6 - 04-02-2020
### Added
- Handle frontend errors nicely when mounting components
### Fixed
- Fixed displaying a remote event when organizer is a group
- Fixed sending events & posts to group followers
- Fixed redirection after deleting an event
## 1.0.5 - 27-01-2020 ## 1.0.5 - 27-01-2020
### Fixed ### Fixed

View file

@ -1,6 +1,6 @@
{ {
"name": "mobilizon", "name": "mobilizon",
"version": "1.0.5", "version": "1.0.6",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

View file

@ -20,7 +20,9 @@
</p> </p>
</b-message> </b-message>
</div> </div>
<main> <error v-if="error" :error="error" />
<main v-else>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<router-view /> <router-view />
</transition> </transition>
@ -57,6 +59,8 @@ import { ICurrentUser } from "./types/current-user.model";
components: { components: {
Logo, Logo,
NavBar, NavBar,
error: () =>
import(/* webpackChunkName: "editor" */ "./components/Error.vue"),
"mobilizon-footer": Footer, "mobilizon-footer": Footer,
}, },
}) })
@ -65,12 +69,18 @@ export default class App extends Vue {
currentUser!: ICurrentUser; currentUser!: ICurrentUser;
error: Error | null = null;
async created(): Promise<void> { async created(): Promise<void> {
if (await this.initializeCurrentUser()) { if (await this.initializeCurrentUser()) {
await initializeCurrentActor(this.$apollo.provider.defaultClient); await initializeCurrentActor(this.$apollo.provider.defaultClient);
} }
} }
errorCaptured(error: Error): void {
this.error = error;
}
private async initializeCurrentUser() { private async initializeCurrentUser() {
const userId = localStorage.getItem(AUTH_USER_ID); const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL); const userEmail = localStorage.getItem(AUTH_USER_EMAIL);

View file

@ -0,0 +1,52 @@
<template>
<p>
<a :title="contact" v-if="configLink" :href="configLink.uri">{{
configLink.text
}}</a>
<span v-else-if="contact">{{ contact }}</span>
<span v-else>{{ $t("contact uninformed") }}</span>
</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class InstanceContactLink extends Vue {
@Prop({ required: true, type: String }) contact!: string;
get configLink(): { uri: string; text: string } | null {
if (!this.contact) return null;
if (this.isContactEmail) {
return {
uri: `mailto:${this.contact}`,
text: this.contact,
};
}
if (this.isContactURL) {
return {
uri: this.contact,
text:
InstanceContactLink.urlToHostname(this.contact) ||
(this.$t("Contact") as string),
};
}
return null;
}
get isContactEmail(): boolean {
return this.contact.includes("@");
}
get isContactURL(): boolean {
return this.contact.match(/^https?:\/\//g) !== null;
}
static urlToHostname(url: string): string | null {
try {
return new URL(url).hostname;
} catch (e) {
return null;
}
}
}
</script>

219
js/src/components/Error.vue Normal file
View file

@ -0,0 +1,219 @@
<template>
<div class="container section" id="error-wrapper">
<div class="column">
<section>
<div class="picture-wrapper">
<picture>
<source
srcset="
/img/pics/error-480w.webp 1x,
/img/pics/error-1024w.webp 2x
"
type="image/webp"
/>
<source
srcset="/img/pics/error-480w.jpg 1x, /img/pics/error-1024w.jpg 2x"
type="image/jpeg"
/>
<img
:src="`/img/pics/error-480w.jpg`"
alt=""
width="480"
height="312"
loading="lazy"
/>
</picture>
</div>
<b-message type="is-danger" class="is-size-5">
<h1>
{{
$t(
"An error has occured. Sorry about that. You may try to reload the page."
)
}}
</h1>
</b-message>
</section>
<b-loading v-if="$apollo.loading" :active.sync="$apollo.loading" />
<section v-else>
<h2 class="is-size-5">{{ $t("What can I do to help?") }}</h2>
<p class="content">
<i18n
tag="span"
path="{instanceName} is an instance of {mobilizon_link}, a free software built with the community."
>
<b slot="instanceName">{{ config.name }}</b>
<a slot="mobilizon_link" href="https://joinmobilizon.org">{{
$t("Mobilizon")
}}</a>
</i18n>
{{
$t(
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
)
}}
</p>
<div class="content">
<ul>
<li>
<a
href="https://framacolibri.org/c/mobilizon/39"
target="_blank"
>{{ $t("Open a topic on our forum") }}</a
>
</li>
<li>
<a
href="https://framagit.org/framasoft/mobilizon/-/issues/new?issuable_template=Bug"
target="_blank"
>{{
$t("Open an issue on our bug tracker (advanced users)")
}}</a
>
</li>
</ul>
</div>
<p class="content">
{{
$t(
"Please add as many details as possible to help identify the problem."
)
}}
</p>
<details>
<summary class="is-size-5">{{ $t("Technical details") }}</summary>
<p>{{ $t("Error message") }}</p>
<pre>{{ error }}</pre>
<p>{{ $t("Error stacktrace") }}</p>
<pre>{{ error.stack }}</pre>
</details>
<p>
{{
$t(
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
)
}}
</p>
<div class="buttons">
<b-tooltip
:label="tooltipConfig.label"
:type="tooltipConfig.type"
:active="copied !== false"
always
>
<b-button @click="copyErrorToClipboard">{{
$t("Copy details to clipboard")
}}</b-button>
</b-tooltip>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import { CONTACT } from "@/graphql/config";
import { Component, Prop, Vue } from "vue-property-decorator";
import InstanceContactLink from "@/components/About/InstanceContactLink.vue";
@Component({
apollo: {
config: {
query: CONTACT,
},
},
metaInfo() {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.$t("Error") as string,
titleTemplate: "%s | Mobilizon",
};
},
components: {
InstanceContactLink,
},
})
export default class ErrorComponent extends Vue {
@Prop({ required: true, type: Error }) error!: Error;
copied: "success" | "error" | false = false;
config!: { contact: string | null; name: string };
async copyErrorToClipboard(): Promise<void> {
try {
if (window.isSecureContext && navigator.clipboard) {
await navigator.clipboard.writeText(this.fullErrorString);
} else {
this.fallbackCopyTextToClipboard(this.fullErrorString);
}
this.copied = "success";
setTimeout(() => {
this.copied = false;
}, 2000);
} catch (e) {
this.copied = "error";
console.error("Unable to copy to clipboard");
console.error(e);
}
}
get fullErrorString(): string {
return `${this.error.name}: ${this.error.message}\n\n${this.error.stack}`;
}
get tooltipConfig(): { label: string | null; type: string | null } {
if (this.copied === "success")
return {
label: this.$t("Error details copied!") as string,
type: "is-success",
};
if (this.copied === "error")
return {
label: this.$t("Unable to copy to clipboard") as string,
type: "is-danger",
};
return { label: null, type: "is-primary" };
}
private fallbackCopyTextToClipboard(text: string): void {
const textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
}
</script>
<style lang="scss" scoped>
#error-wrapper {
width: 100%;
background: $white;
section {
margin-bottom: 2rem;
}
.picture-wrapper {
text-align: center;
}
details {
summary:hover {
cursor: pointer;
}
}
}
</style>

View file

@ -112,6 +112,15 @@ export const ABOUT = gql`
} }
`; `;
export const CONTACT = gql`
query Contact {
config {
name
contact
}
}
`;
export const RULES = gql` export const RULES = gql`
query Rules { query Rules {
config { config {

View file

@ -836,5 +836,19 @@
"No follower matches the filters": "No follower matches the filters", "No follower matches the filters": "No follower matches the filters",
"@{username}'s follow request was rejected": "@{username}'s follow request was rejected", "@{username}'s follow request was rejected": "@{username}'s follow request was rejected",
"Followers will receive new public events and posts.": "Followers will receive new public events and posts.", "Followers will receive new public events and posts.": "Followers will receive new public events and posts.",
"Manually approve new followers": "Manually approve new followers" "Manually approve new followers": "Manually approve new followers",
"An error has occured. Sorry about that. You may try to reload the page.": "An error has occured. Sorry about that. You may try to reload the page.",
"What can I do to help?": "What can I do to help?",
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):'",
"Please add as many details as possible to help identify the problem.": "Please add as many details as possible to help identify the problem.",
"Technical details": "Technical details",
"Error message": "Error message",
"Error stacktrace": "Error stacktrace",
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback.": "The technical details of the error can help developers solve the problem more easily. Please add them to your feedback.",
"Error details copied!": "Error details copied!",
"Copy details to clipboard": "Copy details to clipboard",
"{instanceName} is an instance of {mobilizon_link}, a free software built with the community.": "{instanceName} is an instance of {mobilizon_link}, a free software built with the community.",
"Open a topic on our forum": "Open a topic on our forum",
"Open an issue on our bug tracker (advanced users)": "Open an issue on our bug tracker (advanced users)",
"Unable to copy to clipboard": "Unable to copy to clipboard"
} }

View file

@ -931,5 +931,19 @@
"No follower matches the filters": "Aucun⋅e abonné⋅e ne correspond aux filtres", "No follower matches the filters": "Aucun⋅e abonné⋅e ne correspond aux filtres",
"@{username}'s follow request was rejected": "La demande de suivi de @{username} a été rejettée", "@{username}'s follow request was rejected": "La demande de suivi de @{username} a été rejettée",
"Followers will receive new public events and posts.": "Les abonnée⋅s recevront les nouveaux événements et billets publics.", "Followers will receive new public events and posts.": "Les abonnée⋅s recevront les nouveaux événements et billets publics.",
"Manually approve new followers": "Approuver les nouvelles demandes de suivi manuellement" "Manually approve new followers": "Approuver les nouvelles demandes de suivi manuellement",
"An error has occured. Sorry about that. You may try to reload the page.": "Une erreur est survenue. Nous en sommes désolé⋅es. Vous pouvez essayer de rafraîchir la page.",
"What can I do to help?": "Que puis-je faire pour aider ?",
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Nous améliorons ce logiciel grâce à vos retours. Pour nous avertir de ce problème, vous avez deux possibilités (les deux requièrent toutefois la création d'un compte) :",
"Please add as many details as possible to help identify the problem.": "Merci d'ajouter un maximum de détails afin d'aider à identifier le problème.",
"Technical details": "Détails techniques",
"Error message": "Message d'erreur",
"Error stacktrace": "Trace d'appels de l'erreur",
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback.": "Les détails techniques de l'erreur peuvent aider les développeur⋅ices à résoudre le problème plus facilement. Merci de les inclure dans vos retours.",
"Error details copied!": "Détails de l'erreur copiés !",
"Copy details to clipboard": "Copier les détails dans le presse-papiers",
"{instanceName} is an instance of {mobilizon_link}, a free software built with the community.": "{instanceName} est une instance de {mobilizon_link}, un logiciel libre construit de manière communautaire.",
"Open a topic on our forum": "Ouvrir un sujet sur notre forum",
"Open an issue on our bug tracker (advanced users)": "Ouvrir un ticket sur notre système de suivi des bugs (utilisateur⋅ices avancé⋅es)",
"Unable to copy to clipboard": "Impossible de copier dans le presse-papiers"
} }

View file

@ -157,5 +157,10 @@ const router = new Router({
}); });
router.beforeEach(authGuardIfNeeded); router.beforeEach(authGuardIfNeeded);
router.afterEach(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
router.app.$children[0].error = null;
});
export default router; export default router;

View file

@ -25,16 +25,7 @@
</div> </div>
<div class="column contact"> <div class="column contact">
<h4>{{ $t("Contact") }}</h4> <h4>{{ $t("Contact") }}</h4>
<p> <instance-contact-link :contact="config.contact" />
<a
:title="config.contact"
v-if="generateConfigLink()"
:href="generateConfigLink().uri"
>{{ generateConfigLink().text }}</a
>
<span v-else-if="config.contact">{{ config.contact }}</span>
<span v-else>{{ $t("contact uninformed") }}</span>
</p>
</div> </div>
</section> </section>
<hr /> <hr />
@ -85,6 +76,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { formatList } from "@/utils/i18n"; import { formatList } from "@/utils/i18n";
import InstanceContactLink from "@/components/About/InstanceContactLink.vue";
import { LANGUAGES_CODES } from "@/graphql/admin"; import { LANGUAGES_CODES } from "@/graphql/admin";
import { ILanguage } from "@/types/admin.model"; import { ILanguage } from "@/types/admin.model";
import { ABOUT } from "../../graphql/config"; import { ABOUT } from "../../graphql/config";
@ -109,6 +101,9 @@ import langs from "../../i18n/langs.json";
}, },
}, },
}, },
components: {
InstanceContactLink,
},
}) })
export default class AboutInstance extends Vue { export default class AboutInstance extends Vue {
config!: IConfig; config!: IConfig;
@ -117,14 +112,6 @@ export default class AboutInstance extends Vue {
languages!: ILanguage[]; languages!: ILanguage[];
get isContactEmail(): boolean {
return this.config && this.config.contact.includes("@");
}
get isContactURL(): boolean {
return this.config && this.config.contact.match(/^https?:\/\//g) !== null;
}
get formattedLanguageList(): string { get formattedLanguageList(): string {
if (this.languages) { if (this.languages) {
const list = this.languages.map(({ name }) => name); const list = this.languages.map(({ name }) => name);
@ -138,33 +125,6 @@ export default class AboutInstance extends Vue {
const languageMaps = langs as Record<string, any>; const languageMaps = langs as Record<string, any>;
return languageMaps[code]; return languageMaps[code];
} }
generateConfigLink(): { uri: string; text: string } | null {
if (!this.config.contact) return null;
if (this.isContactEmail) {
return {
uri: `mailto:${this.config.contact}`,
text: this.config.contact,
};
}
if (this.isContactURL) {
return {
uri: this.config.contact,
text:
AboutInstance.urlToHostname(this.config.contact) ||
(this.$t("Contact") as string),
};
}
return null;
}
static urlToHostname(url: string): string | null {
try {
return new URL(url).hostname;
} catch (e) {
return null;
}
}
} }
</script> </script>

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="container"> <div class="container">
<b-loading :active.sync="$apollo.loading" />
<transition appear name="fade" mode="out-in"> <transition appear name="fade" mode="out-in">
<div> <div>
<div <div
@ -125,9 +124,9 @@
<b-icon icon="link" /> <b-icon icon="link" />
</p> </p>
</template> </template>
<template v-if="!event.local"> <template v-if="!event.local && organizer">
<a :href="event.url"> <a :href="event.url">
<tag>{{ event.organizerActor.domain }}</tag> <tag>{{ organizer.domain }}</tag>
</a> </a>
</template> </template>
<p> <p>
@ -443,7 +442,7 @@
<report-modal <report-modal
:on-confirm="reportEvent" :on-confirm="reportEvent"
:title="$t('Report this event')" :title="$t('Report this event')"
:outside-domain="domainForReport" :outside-domain="organizerDomain"
@close="$refs.reportModal.close()" @close="$refs.reportModal.close()"
/> />
</b-modal> </b-modal>
@ -942,7 +941,7 @@ export default class Event extends EventMixin {
}); });
}); });
this.$on("eventDeleted", () => { this.$on("event-deleted", () => {
return this.$router.push({ name: RouteName.HOME }); return this.$router.push({ name: RouteName.HOME });
}); });
} }
@ -959,7 +958,7 @@ export default class Event extends EventMixin {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.$refs.reportModal.close(); this.$refs.reportModal.close();
if (!this.event.organizerActor) return; if (!this.organizer) return;
const eventTitle = this.event.title; const eventTitle = this.event.title;
try { try {
@ -967,7 +966,7 @@ export default class Event extends EventMixin {
mutation: CREATE_REPORT, mutation: CREATE_REPORT,
variables: { variables: {
eventId: this.event.id, eventId: this.event.id,
reportedId: this.actorForReport ? this.actorForReport.id : null, reportedId: this.organizer ? this.organizer.id : null,
content, content,
forward, forward,
}, },
@ -1240,7 +1239,7 @@ export default class Event extends EventMixin {
); );
} }
get actorForReport(): IActor | null { get organizer(): IActor | null {
if (this.event.attributedTo && this.event.attributedTo.id) { if (this.event.attributedTo && this.event.attributedTo.id) {
return this.event.attributedTo; return this.event.attributedTo;
} }
@ -1250,9 +1249,9 @@ export default class Event extends EventMixin {
return null; return null;
} }
get domainForReport(): string | null { get organizerDomain(): string | null {
if (this.actorForReport) { if (this.organizer) {
return this.actorForReport.domain; return this.organizer.domain;
} }
return null; return null;
} }

File diff suppressed because it is too large Load diff

View file

@ -710,6 +710,8 @@ defmodule Mobilizon.Federation.ActivityPub do
Relay.publish(activity) Relay.publish(activity)
end end
recipients = Enum.uniq(recipients)
{recipients, followers} = convert_followers_in_recipients(recipients) {recipients, followers} = convert_followers_in_recipients(recipients)
{recipients, members} = convert_members_in_recipients(recipients) {recipients, members} = convert_members_in_recipients(recipients)

View file

@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Posts.Post
alias Mobilizon.Share alias Mobilizon.Share
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
@ -64,10 +65,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{mentions, []} {mentions, []}
end 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 get_addressed_actors(mentioned_users, _), do: mentioned_users
def calculate_to_and_cc_from_mentions( def calculate_to_and_cc_from_mentions(
@ -79,9 +76,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
end end
def calculate_to_and_cc_from_mentions(%Comment{} = comment) do def calculate_to_and_cc_from_mentions(%Comment{} = comment) do
with mentioned_actors <- Enum.map(comment.mentions, &process_mention/1), with {to, cc} <-
addressed_actors <- get_addressed_actors(mentioned_actors, nil), extract_actors_from_mentions(comment.mentions, comment.actor, comment.visibility),
{to, cc} <- get_to_and_cc(comment.actor, addressed_actors, comment.visibility),
{to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc}, {to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc},
{to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc}, {to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc},
{to, cc} <- {to, cc} <-
@ -101,32 +97,45 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
end end
end end
def calculate_to_and_cc_from_mentions(%Event{ def calculate_to_and_cc_from_mentions(
%Event{
attributed_to: %Actor{members_url: members_url}, attributed_to: %Actor{members_url: members_url},
visibility: visibility visibility: visibility
}) do } = event
) do
%{"to" => to, "cc" => cc} = extract_actors_from_event(event)
case visibility do case visibility do
:public -> :public ->
%{"to" => [members_url, @ap_public], "cc" => []} %{"to" => [@ap_public, members_url] ++ to, "cc" => [] ++ cc}
:unlisted -> :unlisted ->
%{"to" => [members_url], "cc" => [@ap_public]} %{"to" => [members_url] ++ to, "cc" => [@ap_public] ++ cc}
:private -> :private ->
# Private is restricted to only the members
%{"to" => [members_url], "cc" => []} %{"to" => [members_url], "cc" => []}
end end
end end
def calculate_to_and_cc_from_mentions(%Event{} = event) do def calculate_to_and_cc_from_mentions(%Event{} = event) do
with mentioned_actors <- Enum.map(event.mentions, &process_mention/1), extract_actors_from_event(event)
addressed_actors <- get_addressed_actors(mentioned_actors, nil), end
{to, cc} <- get_to_and_cc(event.organizer_actor, addressed_actors, event.visibility),
{to, cc} <- def calculate_to_and_cc_from_mentions(%Post{
{to, attributed_to: %Actor{members_url: members_url},
Enum.uniq( visibility: visibility
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) }) do
)} do case visibility do
%{"to" => to, "cc" => cc} :public ->
%{"to" => [@ap_public, members_url], "cc" => []}
:unlisted ->
%{"to" => [members_url], "cc" => [@ap_public]}
:private ->
# Private is restricted to only the members
%{"to" => [members_url], "cc" => []}
end end
end end
@ -211,4 +220,27 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
url url
end end
end end
@spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()}
defp extract_actors_from_mentions(mentions, actor, visibility) do
with mentioned_actors <- Enum.map(mentions, &process_mention/1),
addressed_actors <- get_addressed_actors(mentioned_actors, nil) do
get_to_and_cc(actor, addressed_actors, visibility)
end
end
defp extract_actors_from_event(%Event{} = event) do
with {to, cc} <-
extract_actors_from_mentions(event.mentions, event.organizer_actor, event.visibility),
{to, cc} <-
{to,
Enum.uniq(
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
)} do
%{"to" => to, "cc" => cc}
else
_ ->
%{"to" => [], "cc" => []}
end
end
end end

View file

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Posts, Tombstone} alias Mobilizon.{Actors, Posts, Tombstone}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -19,15 +20,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
Posts.create_post(args), Posts.create_post(args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), %Actor{} = creator <- Actors.get_actor(creator_id),
post_as_data <- post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}), Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
audience <- %{ audience <-
"to" => [group.members_url], Audience.calculate_to_and_cc_from_mentions(post) do
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
} do
create_data = make_create_data(post_as_data, Map.merge(audience, additional)) create_data = make_create_data(post_as_data, Map.merge(audience, additional))
{:ok, post, create_data} {:ok, post, create_data}
@ -44,16 +41,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
Posts.update_post(post, args), Posts.update_post(post, args),
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"), {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
{:ok, %Actor{url: group_url} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), %Actor{} = creator <- Actors.get_actor(creator_id),
post_as_data <- post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}), Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
audience <- %{ audience <-
"to" => [group.members_url], Audience.calculate_to_and_cc_from_mentions(post) do
"cc" => [],
"actor" => creator_url,
"attributedTo" => [group_url]
} do
update_data = make_update_data(post_as_data, Map.merge(audience, additional)) update_data = make_update_data(post_as_data, Map.merge(audience, additional))
{:ok, post, update_data} {:ok, post, update_data}

View file

@ -127,7 +127,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_relay_if_group_activity(activity, attributed_to \\ nil) def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
def maybe_relay_if_group_activity( def maybe_relay_if_group_activity(
%Activity{local: false, data: %{"object" => object}}, %Activity{data: %{"object" => object}},
_attributed_to _attributed_to
) )
when is_map(object) do when is_map(object) do
@ -136,7 +136,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
# When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well # When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well
def maybe_relay_if_group_activity( def maybe_relay_if_group_activity(
%Activity{local: false, data: %{"object" => object}}, %Activity{data: %{"object" => object}},
%Actor{url: attributed_to_url} %Actor{url: attributed_to_url}
) )
when is_binary(object) do when is_binary(object) do
@ -421,7 +421,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
["https://www.w3.org/ns/activitystreams#Public"]} ["https://www.w3.org/ns/activitystreams#Public"]}
else else
if actor_type == :Group do if actor_type == :Group do
{[actor.members_url], []} {[actor.followers_url, actor.members_url], []}
else else
{[actor.followers_url], []} {[actor.followers_url], []}
end end

View file

@ -94,18 +94,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
to = to =
if event.visibility == :public, if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"], do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [event.organizer_actor.followers_url] else: [attributed_to_or_default(event).followers_url]
%{ %{
"type" => "Event", "type" => "Event",
"to" => to, "to" => to,
"cc" => [], "cc" => [],
"attributedTo" => "attributedTo" => attributed_to_or_default(event).url,
if(is_nil(event.attributed_to) or not Ecto.assoc_loaded?(event.attributed_to),
do: nil,
else: event.attributed_to.url
) ||
event.organizer_actor.url,
"name" => event.title, "name" => event.title,
"actor" => "actor" =>
if(Ecto.assoc_loaded?(event.organizer_actor), do: event.organizer_actor.url, else: nil), if(Ecto.assoc_loaded?(event.organizer_actor), do: event.organizer_actor.url, else: nil),
@ -135,6 +130,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|> maybe_add_inline_media(event) |> maybe_add_inline_media(event)
end end
@spec attributed_to_or_default(Event.t()) :: Actor.t()
defp attributed_to_or_default(event) do
if(is_nil(event.attributed_to) or not Ecto.assoc_loaded?(event.attributed_to),
do: nil,
else: event.attributed_to
) ||
event.organizer_actor
end
# Get only elements that we have in EventOptions # Get only elements that we have in EventOptions
@spec get_options(map) :: map @spec get_options(map) :: map
defp get_options(object) do defp get_options(object) do

View file

@ -34,10 +34,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
@impl Converter @impl Converter
@spec model_to_as(Post.t()) :: map @spec model_to_as(Post.t()) :: map
def model_to_as( def model_to_as(
%Post{author: %Actor{url: actor_url}, attributed_to: %Actor{url: creator_url}} = post %Post{
author: %Actor{url: actor_url},
attributed_to: %Actor{url: creator_url, followers_url: followers_url}
} = post
) do ) do
to =
if post.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [followers_url]
%{ %{
"type" => "Article", "type" => "Article",
"to" => to,
"cc" => [],
"actor" => actor_url, "actor" => actor_url,
"id" => post.url, "id" => post.url,
"name" => post.title, "name" => post.title,

View file

@ -56,7 +56,7 @@ defmodule Mobilizon.Posts.Post do
field(:title, :string) field(:title, :string)
field(:url, :string) field(:url, :string)
field(:publish_at, :utc_datetime) field(:publish_at, :utc_datetime)
field(:visibility, PostVisibility, default_value: :public) field(:visibility, PostVisibility, default: :public)
belongs_to(:author, Actor) belongs_to(:author, Actor)
belongs_to(:attributed_to, Actor) belongs_to(:attributed_to, Actor)
belongs_to(:picture, Media, on_replace: :update) belongs_to(:picture, Media, on_replace: :update)

View file

@ -1,7 +1,7 @@
defmodule Mobilizon.Mixfile do defmodule Mobilizon.Mixfile do
use Mix.Project use Mix.Project
@version "1.0.5" @version "1.0.6"
def project do def project do
[ [

View file

@ -1,5 +1,5 @@
%{ %{
"absinthe": {:hex, :absinthe, "1.6.0", "7cb42eebbb9cbf5077541d73c189e205ebe12caf1c78372fc5b9e706fc8ac298", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "99915841495522332b3af8ff10c9cbb51e256b28d9b19c0dfaac5f044b6bfb66"}, "absinthe": {:hex, :absinthe, "1.6.1", "07bd1636027595c8d00d250a5878e617c24ccb25c84a08e807d8d00cf124696c", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f0105f1de6176ca50789081f2465389cb7afa438d54bf5133aeb3549f8f629f6"},
"absinthe_phoenix": {:git, "https://github.com/absinthe-graphql/absinthe_phoenix.git", "67dc53db5b826ea12f37860bcce4334d4aaad028", [ref: "67dc53db5b826ea12f37860bcce4334d4aaad028"]}, "absinthe_phoenix": {:git, "https://github.com/absinthe-graphql/absinthe_phoenix.git", "67dc53db5b826ea12f37860bcce4334d4aaad028", [ref: "67dc53db5b826ea12f37860bcce4334d4aaad028"]},
"absinthe_plug": {:hex, :absinthe_plug, "1.5.4", "daff02d04be7c06d0114ef5b4361865a4dacbe8ddb325ce709b103253d4a014b", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "80360cd8ad541d87c75336f3abc59b894d474458f6a7f8e563781c01148860de"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.4", "daff02d04be7c06d0114ef5b4361865a4dacbe8ddb325ce709b103253d4a014b", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "80360cd8ad541d87c75336f3abc59b894d474458f6a7f8e563781c01148860de"},
"argon2_elixir": {:hex, :argon2_elixir, "2.4.0", "2a22ea06e979f524c53b42b598fc6ba38cdcbc977a155e33e057732cfb1fb311", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "4ea82e183cf8e7f66dab1f767fedcfe6a195e140357ef2b0423146b72e0a551d"}, "argon2_elixir": {:hex, :argon2_elixir, "2.4.0", "2a22ea06e979f524c53b42b598fc6ba38cdcbc977a155e33e057732cfb1fb311", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "4ea82e183cf8e7f66dab1f767fedcfe6a195e140357ef2b0423146b72e0a551d"},