Add group admin profiles

And other fixes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-08-27 11:53:24 +02:00
parent 8afda73214
commit 1984f71cbf
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
107 changed files with 3514 additions and 1146 deletions

View file

@ -1,24 +1,14 @@
@import "variables.scss";
a { a {
color: $violet-2;
}
a.out,
.content a {
text-decoration: underline; text-decoration: underline;
text-decoration-color: #ed8d07; text-decoration-color: #ed8d07;
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
&.navbar-item,
&.dropdown-item,
&.card,
&.button,
&[href="#comments"],
&.router-link-active,
&.comment-link,
&.pagination-link,
&.datepicker-cell,
&.list-item {
text-decoration: none;
}
}
nav.breadcrumb ul li a {
text-decoration: none;
} }
input.input { input.input {

View file

@ -15,8 +15,8 @@
<div class="title-info-wrapper"> <div class="title-info-wrapper">
<div class="title-and-date"> <div class="title-and-date">
<p class="discussion-minimalist-title">{{ discussion.title }}</p> <p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="discussion.updatedAt | formatDateTimeString"> <span :title="actualDate | formatDateTimeString">
{{ $timeAgo.format(new Date(discussion.updatedAt), "twitter") || $t("Right now") }}</span {{ $timeAgo.format(new Date(actualDate), "twitter") || $t("Right now") }}</span
> >
</div> </div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt"> <div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
@ -46,6 +46,13 @@ export default class DiscussionListItem extends Vue {
} }
return element.innerText; return element.innerText;
} }
get actualDate() {
if (this.discussion.updatedAt === this.discussion.insertedAt && this.discussion.lastComment) {
return this.discussion.lastComment.publishedAt;
}
return this.discussion.updatedAt;
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -37,6 +37,7 @@
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" /> <SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" /> <SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" /> <SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
</SettingMenuSection> </SettingMenuSection>
<SettingMenuSection <SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR" v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"

View file

@ -5,6 +5,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
id id
title title
slug slug
insertedAt
updatedAt updatedAt
lastComment { lastComment {
id id
@ -16,6 +17,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql`
url url
} }
} }
publishedAt
deletedAt deletedAt
} }
} }
@ -114,6 +116,7 @@ export const GET_DISCUSSION = gql`
insertedAt insertedAt
updatedAt updatedAt
deletedAt deletedAt
publishedAt
} }
} }
...DiscussionFields ...DiscussionFields

View file

@ -4,8 +4,24 @@ import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "./resources";
import { POST_BASIC_FIELDS } from "./post"; import { POST_BASIC_FIELDS } from "./post";
export const LIST_GROUPS = gql` export const LIST_GROUPS = gql`
query { query ListGroups(
groups { $preferredUsername: String
$name: String
$domain: String
$local: Boolean
$suspended: Boolean
$page: Int
$limit: Int
) {
groups(
preferredUsername: $preferredUsername
name: $name
domain: $domain
local: $local
suspended: $suspended
page: $page
limit: $limit
) {
elements { elements {
id id
url url
@ -34,109 +50,128 @@ export const LIST_GROUPS = gql`
} }
`; `;
export const GROUP_FIELDS_FRAGMENTS = gql`
fragment GroupFullFields on Group {
id
url
name
domain
summary
preferredUsername
suspended
visibility
physicalAddress {
description
street
locality
postalCode
region
country
geom
type
id
originId
}
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
id
uuid
title
beginsOn
}
total
}
discussions {
total
elements {
...DiscussionBasicFields
}
}
posts {
total
elements {
...PostBasicFields
}
}
members {
elements {
role
actor {
id
name
domain
preferredUsername
avatar {
url
}
}
insertedAt
}
total
}
resources(page: 1, limit: 3) {
elements {
id
title
resourceUrl
summary
updatedAt
type
path
metadata {
...ResourceMetadataBasicFields
}
}
total
}
todoLists {
elements {
id
title
todos {
elements {
id
title
status
dueDate
assignedTo {
id
preferredUsername
}
}
total
}
}
total
}
}
`;
export const FETCH_GROUP = gql` export const FETCH_GROUP = gql`
query($name: String!) { query($name: String!) {
group(preferredUsername: $name) { group(preferredUsername: $name) {
id ...GroupFullFields
url
name
domain
summary
preferredUsername
suspended
visibility
physicalAddress {
description
street
locality
postalCode
region
country
geom
type
id
originId
}
avatar {
url
}
banner {
url
}
organizedEvents {
elements {
id
uuid
title
beginsOn
}
total
}
discussions {
total
elements {
...DiscussionBasicFields
}
}
posts {
total
elements {
...PostBasicFields
}
}
members {
elements {
role
actor {
id
name
domain
preferredUsername
avatar {
url
}
}
insertedAt
}
total
}
resources(page: 1, limit: 3) {
elements {
id
title
resourceUrl
summary
updatedAt
type
path
metadata {
...ResourceMetadataBasicFields
}
}
total
}
todoLists {
elements {
id
title
todos {
elements {
id
title
status
dueDate
assignedTo {
id
preferredUsername
}
}
total
}
}
total
}
} }
} }
${GROUP_FIELDS_FRAGMENTS}
${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${POST_BASIC_FIELDS}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const GET_GROUP = gql`
query($id: ID!) {
getGroup(id: $id) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
${DISCUSSION_BASIC_FIELDS_FRAGMENT} ${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${POST_BASIC_FIELDS} ${POST_BASIC_FIELDS}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
@ -162,6 +197,7 @@ export const CREATE_GROUP = gql`
id id
preferredUsername preferredUsername
name name
domain
summary summary
avatar { avatar {
url url
@ -206,6 +242,14 @@ export const UPDATE_GROUP = gql`
} }
`; `;
export const DELETE_GROUP = gql`
mutation DeleteGroup($groupId: ID!) {
deleteGroup(groupId: $groupId) {
id
}
}
`;
export const LEAVE_GROUP = gql` export const LEAVE_GROUP = gql`
mutation LeaveGroup($groupId: ID!) { mutation LeaveGroup($groupId: ID!) {
leaveGroup(groupId: $groupId) { leaveGroup(groupId: $groupId) {
@ -213,3 +257,11 @@ export const LEAVE_GROUP = gql`
} }
} }
`; `;
export const REFRESH_PROFILE = gql`
mutation RefreshProfile($actorId: ID!) {
refreshProfile(id: $actorId) {
id
}
}
`;

View file

@ -160,7 +160,6 @@
"Go": "Go", "Go": "Go",
"Going as {name}": "Going as {name}", "Going as {name}": "Going as {name}",
"Group List": "Group List", "Group List": "Group List",
"Group full name": "Group full name",
"Group name": "Group name", "Group name": "Group name",
"Group {displayName} created": "Group {displayName} created", "Group {displayName} created": "Group {displayName} created",
"Groups": "Groups", "Groups": "Groups",
@ -766,5 +765,19 @@
"Edited {ago}": "Edited {ago}", "Edited {ago}": "Edited {ago}",
"[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]", "[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]",
"Promote": "Promote", "Promote": "Promote",
"Demote": "Demote" "Demote": "Demote",
"{number} members": "{number} members",
"{number} posts": "No posts|One post|{number} posts",
"Publication date": "Publication date",
"Refresh profile": "Refresh profile",
"Suspend group": "Suspend group",
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.": "Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.",
"Delete group": "Delete group",
"Are you sure you want to <b>completely delete</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Are you sure you want to <b>completely delete</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.",
"Group display name": "Group display name",
"Federated Group Name": "Federated Group Name",
"This is like your federated username (<code>{username}</code>) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.": "This is like your federated username (<code>{username}</code>) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.",
"Banner": "Banner",
"A group with this name already exists": "A group with this name already exists"
} }

View file

@ -243,11 +243,10 @@
"Group List": "Liste de groupes", "Group List": "Liste de groupes",
"Group Members": "Membres du groupe", "Group Members": "Membres du groupe",
"Group address": "Adresse du groupe", "Group address": "Adresse du groupe",
"Group full name": "Nom complet du groupe",
"Group name": "Nom du groupe", "Group name": "Nom du groupe",
"Group settings": "Paramètres du groupe", "Group settings": "Paramètres du groupe",
"Group short description": "Description courte du groupe", "Group short description": "Description courte du groupe",
"Group visibility": "Visibility du groupe", "Group visibility": "Visibilité du groupe",
"Group {displayName} created": "Groupe {displayName} créé", "Group {displayName} created": "Groupe {displayName} créé",
"Groups": "Groupes", "Groups": "Groupes",
"Headline picture": "Image à la une", "Headline picture": "Image à la une",
@ -301,7 +300,7 @@
"Language": "Langue", "Language": "Langue",
"Last published event": "Dernier évènement publié", "Last published event": "Dernier évènement publié",
"Last week": "La semaine dernière", "Last week": "La semaine dernière",
"Latest posts": "Derniers messages publics", "Latest posts": "Derniers billets",
"Learn more": "En apprendre plus", "Learn more": "En apprendre plus",
"Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon", "Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon",
"Leave event": "Annuler ma participation à l'évènement", "Leave event": "Annuler ma participation à l'évènement",
@ -767,5 +766,19 @@
"Edited {ago}": "Édité {ago}", "Edited {ago}": "Édité {ago}",
"[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]", "[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]",
"Promote": "Promouvoir", "Promote": "Promouvoir",
"Demote": "Rétrograder" "Demote": "Rétrograder",
"{number} members": "{number} membres",
"{number} posts": "Aucun billet|Un billet|{number} billets",
"Publication date": "Date de publication",
"Refresh profile": "Rafraîchir le profil",
"Suspend group": "Suspendre le groupe",
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Comme ce groupe provient de l'instance {instance}, cela supprimera seulement les membres locaux et supprimera les données locales, et rejettera également toutes les données futures.",
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Tous les membres - y compris celleux sur d'autres instances - seront notifié·es et supprimé·es du groupe, et <b>toutes les données associées au groupe (événements, billets, discussions, todos…) seront irrémédiablement détruites</b>.",
"Delete group": "Supprimer le groupe",
"Are you sure you want to <b>completely delete</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Êtes-vous certain·e de vouloir <b>complètement supprimer</b> ce groupe ? Tous les membres - y compris celleux sur d'autres instances - seront notifié·es et supprimé·es du groupe, et <b>toutes les données associées au groupe (événements, billets, discussions, todos…) seront irrémédiablement détruites</b>.",
"Group display name": "Nom d'affichage du groupe",
"Federated Group Name": "Nom fédéré du groupe",
"This is like your federated username (<code>{username}</code>) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.": "C'est comme votre addresse fédérée (<code>{username}</code>) pour les groupes. Cela vous permettra d'être trouvable sur la fédération, et est garanti d'être unique.",
"Banner": "Bannière",
"A group with this name already exists": "Un groupe avec ce nom existe déjà"
} }

View file

@ -1,7 +1,8 @@
import { Component, Mixins, Vue } from "vue-property-decorator"; import { Component, Mixins, Vue } from "vue-property-decorator";
import { Person } from "@/types/actor"; import { Person } from "@/types/actor";
@Component({}) // TODO: Refactor into js/src/utils/username.ts
@Component
export default class IdentityEditionMixin extends Mixins(Vue) { export default class IdentityEditionMixin extends Mixins(Vue) {
identity: Person = new Person(); identity: Person = new Person();

View file

@ -16,6 +16,8 @@ import Users from "../views/Admin/Users.vue";
import Profiles from "../views/Admin/Profiles.vue"; import Profiles from "../views/Admin/Profiles.vue";
import AdminProfile from "../views/Admin/AdminProfile.vue"; import AdminProfile from "../views/Admin/AdminProfile.vue";
import AdminUserProfile from "../views/Admin/AdminUserProfile.vue"; import AdminUserProfile from "../views/Admin/AdminUserProfile.vue";
import GroupProfiles from "../views/Admin/GroupProfiles.vue";
import AdminGroupProfile from "../views/Admin/AdminGroupProfile.vue";
export enum SettingsRouteName { export enum SettingsRouteName {
SETTINGS = "SETTINGS", SETTINGS = "SETTINGS",
@ -33,6 +35,8 @@ export enum SettingsRouteName {
PROFILES = "PROFILES", PROFILES = "PROFILES",
ADMIN_PROFILE = "ADMIN_PROFILE", ADMIN_PROFILE = "ADMIN_PROFILE",
ADMIN_USER_PROFILE = "ADMIN_USER_PROFILE", ADMIN_USER_PROFILE = "ADMIN_USER_PROFILE",
ADMIN_GROUPS = "ADMIN_GROUPS",
ADMIN_GROUP_PROFILE = "ADMIN_GROUP_PROFILE",
MODERATION = "MODERATION", MODERATION = "MODERATION",
REPORTS = "Reports", REPORTS = "Reports",
REPORT = "Report", REPORT = "Report",
@ -123,6 +127,20 @@ export const settingsRoutes: RouteConfig[] = [
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{
path: "admin/groups",
name: SettingsRouteName.ADMIN_GROUPS,
component: GroupProfiles,
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/groups/:id",
name: SettingsRouteName.ADMIN_GROUP_PROFILE,
component: AdminGroupProfile,
props: true,
meta: { requiredAuth: true },
},
{ {
path: "admin/relays", path: "admin/relays",
name: SettingsRouteName.RELAYS, name: SettingsRouteName.RELAYS,

View file

@ -60,9 +60,11 @@ export class Actor implements IActor {
} }
} }
export function usernameWithDomain(actor: IActor): string { export function usernameWithDomain(actor: IActor, force = false): string {
if (actor.domain) { if (actor.domain) {
return `${actor.preferredUsername}@${actor.domain}`; return `${actor.preferredUsername}@${actor.domain}`;
} else if (force) {
return `${actor.preferredUsername}@${window.location.hostname}`;
} }
return actor.preferredUsername; return actor.preferredUsername;
} }

View file

@ -16,6 +16,7 @@ export interface IComment {
deletedAt?: Date | string; deletedAt?: Date | string;
totalReplies: number; totalReplies: number;
insertedAt?: Date | string; insertedAt?: Date | string;
publishedAt?: Date | string;
} }
export class CommentModel implements IComment { export class CommentModel implements IComment {

View file

@ -10,6 +10,8 @@ export interface IDiscussion {
actor?: IActor; actor?: IActor;
lastComment?: IComment; lastComment?: IComment;
comments: Paginate<IComment>; comments: Paginate<IComment>;
updatedAt: string;
insertedAt: string;
} }
export class Discussion implements IDiscussion { export class Discussion implements IDiscussion {
@ -27,6 +29,10 @@ export class Discussion implements IDiscussion {
lastComment?: IComment = undefined; lastComment?: IComment = undefined;
insertedAt: string = "";
updatedAt: string = "";
constructor(hash?: IDiscussion) { constructor(hash?: IDiscussion) {
if (!hash) return; if (!hash) return;
@ -40,5 +46,7 @@ export class Discussion implements IDiscussion {
this.creator = hash.creator; this.creator = hash.creator;
this.actor = hash.actor; this.actor = hash.actor;
this.lastComment = hash.lastComment; this.lastComment = hash.lastComment;
this.insertedAt = hash.insertedAt;
this.updatedAt = hash.updatedAt;
} }
} }

29
js/src/utils/username.ts Normal file
View file

@ -0,0 +1,29 @@
import { IActor } from "@/types/actor";
function autoUpdateUsername(actor: IActor, newDisplayName: string | null): IActor {
const oldUsername = convertToUsername(actor.name);
if (actor.preferredUsername === oldUsername) {
actor.preferredUsername = convertToUsername(newDisplayName);
}
return actor;
}
function convertToUsername(value: string | null): string {
if (!value) return "";
// https://stackoverflow.com/a/37511463
return value
.toLocaleLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ /g, "_")
.replace(/[^a-z0-9_]/g, "");
}
function validateUsername(actor: IActor): boolean {
return actor.preferredUsername === convertToUsername(actor.preferredUsername);
}
export { autoUpdateUsername, convertToUsername, validateUsername };

View file

@ -50,11 +50,11 @@
</form> </form>
<div v-if="validationSent && !userAlreadyActivated"> <div v-if="validationSent && !userAlreadyActivated">
<b-message title="Success" type="is-success" closable="false"> <b-message type="is-success" closable="false">
<h2 class="title"> <h2 class="title">
{{ {{
$t("Your account is nearly ready, {username}", { $t("Your account is nearly ready, {username}", {
username: identity.preferredUsername, username: identity.name || identity.preferredUsername,
}) })
}} }}
</h2> </h2>

View file

@ -150,6 +150,7 @@ import identityEditionMixin from "../../../mixins/identityEdition";
}, },
identity: { identity: {
query: FETCH_PERSON, query: FETCH_PERSON,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
username: this.identityName, username: this.identityName,

View file

@ -0,0 +1,436 @@
<template>
<div v-if="group" class="section">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
</li>
<li>
<router-link
:to="{
name: RouteName.ADMIN_GROUPS,
}"
>{{ $t("Groups") }}</router-link
>
</li>
<li class="is-active">
<router-link
:to="{
name: RouteName.PROFILES,
params: { id: group.id },
}"
>{{ group.name || group.preferredUsername }}</router-link
>
</li>
</ul>
</nav>
<div class="actor-card">
<router-link
:to="{ name: RouteName.GROUP, params: { preferredUsername: usernameWithDomain(group) } }"
>
<actor-card :actor="group" :full="true" :popover="false" :limit="false" />
</router-link>
</div>
<table v-if="metadata.length > 0" class="table is-fullwidth">
<tbody>
<tr v-for="{ key, value, link } in metadata" :key="key">
<td>{{ key }}</td>
<td v-if="link">
<router-link :to="link">
{{ value }}
</router-link>
</td>
<td v-else>{{ value }}</td>
</tr>
</tbody>
</table>
<div class="buttons">
<b-button @click="confirmSuspendProfile" v-if="!group.suspended" type="is-primary">{{
$t("Suspend")
}}</b-button>
<b-button @click="unsuspendProfile" v-if="group.suspended" type="is-primary">{{
$t("Unsuspend")
}}</b-button>
<b-button @click="refreshProfile" v-if="group.domain" type="is-primary" outlined>{{
$t("Refresh profile")
}}</b-button>
</div>
<section>
<h2 class="subtitle">
{{
$tc("{number} members", group.members.total, {
number: group.members.total,
})
}}
</h2>
<b-table
:data="group.members.elements"
:loading="$apollo.queries.group.loading"
paginated
backend-pagination
:total="group.members.total"
:per-page="EVENTS_PER_PAGE"
@page-change="onMembersPageChange"
>
<b-table-column field="actor.preferredUsername" :label="$t('Member')" v-slot="props">
<article class="media">
<figure class="media-left image is-48x48" v-if="props.row.actor.avatar">
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<div class="media-content">
<div class="content">
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
><br />
<span class="is-size-7 has-text-grey"
>@{{ usernameWithDomain(props.row.actor) }}</span
>
</div>
</div>
</article>
</b-table-column>
<b-table-column field="role" :label="$t('Role')" v-slot="props">
<b-tag type="is-primary" v-if="props.row.role === MemberRole.ADMINISTRATOR">
{{ $t("Administrator") }}
</b-tag>
<b-tag type="is-primary" v-else-if="props.row.role === MemberRole.MODERATOR">
{{ $t("Moderator") }}
</b-tag>
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
{{ $t("Member") }}
</b-tag>
<b-tag type="is-warning" v-else-if="props.row.role === MemberRole.NOT_APPROVED">
{{ $t("Not approved") }}
</b-tag>
<b-tag type="is-danger" v-else-if="props.row.role === MemberRole.REJECTED">
{{ $t("Rejected") }}
</b-tag>
<b-tag type="is-danger" v-else-if="props.row.role === MemberRole.INVITED">
{{ $t("Invited") }}
</b-tag>
</b-table-column>
<b-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
<span class="has-text-centered">
{{ props.row.insertedAt | formatDateString }}<br />{{
props.row.insertedAt | formatTimeString
}}
</span>
</b-table-column>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>{{ $t("Nothing to see here") }}</p>
</div>
</section>
</template>
</b-table>
</section>
<section>
<h2 class="subtitle">
{{
$tc("{number} organized events", group.organizedEvents.total, {
number: group.organizedEvents.total,
})
}}
</h2>
<b-table
:data="group.organizedEvents.elements"
:loading="$apollo.queries.group.loading"
paginated
backend-pagination
:total="group.organizedEvents.total"
:per-page="EVENTS_PER_PAGE"
@page-change="onOrganizedEventsPageChange"
>
<b-table-column field="title" :label="$t('Title')" v-slot="props">
<router-link :to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }">
{{ props.row.title }}
</router-link>
</b-table-column>
<b-table-column field="beginsOn" :label="$t('Begins on')" v-slot="props">
{{ props.row.beginsOn | formatDateTimeString }}
</b-table-column>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>{{ $t("Nothing to see here") }}</p>
</div>
</section>
</template>
</b-table>
</section>
<section>
<h2 class="subtitle">
{{
$tc("{number} posts", group.posts.total, {
number: group.posts.total,
})
}}
</h2>
<b-table
:data="group.posts.elements"
:loading="$apollo.queries.group.loading"
paginated
backend-pagination
:total="group.posts.total"
:per-page="EVENTS_PER_PAGE"
@page-change="onPostsPageChange"
>
<b-table-column field="title" :label="$t('Title')" v-slot="props">
<router-link :to="{ name: RouteName.POST, params: { slug: props.row.slug } }">
{{ props.row.title }}
</router-link>
</b-table-column>
<b-table-column field="publishAt" :label="$t('Publication date')" v-slot="props">
{{ props.row.publishAt | formatDateTimeString }}
</b-table-column>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>{{ $t("Nothing to see here") }}</p>
</div>
</section>
</template>
</b-table>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
import { IGroup, MemberRole } from "../../types/actor";
import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
import RouteName from "../../router/name";
import { IEvent } from "../../types/event.model";
import ActorCard from "../../components/Account/ActorCard.vue";
import { GET_GROUP, REFRESH_PROFILE } from "@/graphql/group";
const EVENTS_PER_PAGE = 10;
@Component({
apollo: {
group: {
query: GET_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.id,
organizedEventsPage: 1,
organizedEventsLimit: EVENTS_PER_PAGE,
};
},
skip() {
return !this.id;
},
update: (data) => data.getGroup,
},
},
components: {
ActorCard,
},
})
export default class AdminGroupProfile extends Vue {
@Prop({ required: true }) id!: string;
group!: IGroup;
usernameWithDomain = usernameWithDomain;
RouteName = RouteName;
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
organizedEventsPage = 1;
membersPage = 1;
postsPage = 1;
MemberRole = MemberRole;
get metadata(): Array<object> {
if (!this.group) return [];
const res: object[] = [
{
key: this.$t("Status") as string,
value: this.group.suspended ? this.$t("Suspended") : this.$t("Active"),
},
{
key: this.$t("Domain") as string,
value: this.group.domain ? this.group.domain : this.$t("Local"),
},
];
return res;
}
confirmSuspendProfile() {
const message = (this.group.domain
? this.$t(
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
{ instance: this.group.domain }
)
: this.$t(
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
)) as string;
this.$buefy.dialog.confirm({
title: this.$t("Suspend group") as string,
message,
confirmText: this.$t("Suspend group") as string,
cancelText: this.$t("Cancel") as string,
type: "is-danger",
hasIcon: true,
onConfirm: () => this.suspendProfile(),
});
}
async suspendProfile() {
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
mutation: SUSPEND_PROFILE,
variables: {
id: this.id,
},
update: (store, { data }) => {
if (data == null) return;
const profileId = this.id;
const profileData = store.readQuery<{ getGroup: IGroup }>({
query: GET_GROUP,
variables: {
id: profileId,
},
});
if (!profileData) return;
const { getGroup: group } = profileData;
group.suspended = true;
group.avatar = null;
group.name = "";
group.summary = "";
store.writeQuery({
query: GET_GROUP,
variables: {
id: profileId,
},
data: { getGroup: group },
});
},
});
}
async unsuspendProfile() {
const profileID = this.id;
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
mutation: UNSUSPEND_PROFILE,
variables: {
id: this.id,
},
refetchQueries: [
{
query: GET_GROUP,
variables: {
id: profileID,
},
},
],
});
}
async refreshProfile() {
this.$apollo.mutate<{ refreshProfile: IActor }>({
mutation: REFRESH_PROFILE,
variables: {
actorId: this.id,
},
});
}
async onOrganizedEventsPageChange(page: number) {
this.organizedEventsPage = page;
await this.$apollo.queries.group.fetchMore({
variables: {
actorId: this.id,
organizedEventsPage: this.organizedEventsPage,
organizedEventsLimit: EVENTS_PER_PAGE,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult;
const newOrganizedEvents = fetchMoreResult.group.organizedEvents.elements;
return {
group: {
...previousResult.group,
organizedEvents: {
__typename: previousResult.group.organizedEvents.__typename,
total: previousResult.group.organizedEvents.total,
elements: [...previousResult.group.organizedEvents.elements, ...newOrganizedEvents],
},
},
};
},
});
}
async onMembersPageChange(page: number) {
this.membersPage = page;
await this.$apollo.queries.group.fetchMore({
variables: {
actorId: this.id,
memberPage: this.membersPage,
memberLimit: EVENTS_PER_PAGE,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult;
const newMembers = fetchMoreResult.group.members.elements;
return {
group: {
...previousResult.group,
members: {
__typename: previousResult.group.members.__typename,
total: previousResult.group.members.total,
elements: [...previousResult.group.members.elements, ...newMembers],
},
},
};
},
});
}
async onPostsPageChange(page: number) {
this.postsPage = page;
await this.$apollo.queries.group.fetchMore({
variables: {
actorId: this.id,
postsPage: this.postsPage,
postLimit: EVENTS_PER_PAGE,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult;
const newPosts = fetchMoreResult.group.posts.elements;
return {
group: {
...previousResult.group,
posts: {
__typename: previousResult.group.posts.__typename,
total: previousResult.group.posts.total,
elements: [...previousResult.group.posts.elements, ...newPosts],
},
},
};
},
});
}
}
</script>
<style lang="scss" scoped>
table,
section {
margin: 2rem 0;
}
.actor-card {
background: #fff;
padding: 1.5rem;
border-radius: 10px;
}
</style>

View file

@ -139,6 +139,7 @@ const EVENTS_PER_PAGE = 10;
apollo: { apollo: {
person: { person: {
query: GET_PERSON, query: GET_PERSON,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
actorId: this.id, actorId: this.id,

View file

@ -69,6 +69,7 @@ import { IPerson } from "../../types/actor";
apollo: { apollo: {
user: { user: {
query: GET_USER, query: GET_USER,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.id, id: this.id,

View file

@ -71,6 +71,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
dashboard: { dashboard: {
query: DASHBOARD, query: DASHBOARD,
fetchPolicy: "cache-and-network",
}, },
}, },
metaInfo() { metaInfo() {

View file

@ -0,0 +1,168 @@
<template>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.PROFILES }">{{ $t("Groups") }}</router-link>
</li>
</ul>
</nav>
<div v-if="groups">
<b-switch v-model="local">{{ $t("Local") }}</b-switch>
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
<b-table
:data="groups.elements"
:loading="$apollo.queries.groups.loading"
paginated
backend-pagination
backend-filtering
:total="groups.total"
:per-page="PROFILES_PER_PAGE"
@page-change="onPageChange"
@filters-change="onFiltersChange"
>
<b-table-column field="preferredUsername" :label="$t('Username')" searchable>
<template slot="searchable" slot-scope="props">
<b-input
v-model="props.filters.preferredUsername"
placeholder="Search..."
icon="magnify"
size="is-small"
/>
</template>
<template v-slot:default="props">
<router-link
class="profile"
:to="{ name: RouteName.ADMIN_GROUP_PROFILE, params: { id: props.row.id } }"
>
<article class="media">
<figure class="media-left" v-if="props.row.avatar">
<p class="image is-48x48">
<img :src="props.row.avatar.url" />
</p>
</figure>
<div class="media-content">
<div class="content">
<strong v-if="props.row.name">{{ props.row.name }}</strong
><br v-if="props.row.name" />
<small>@{{ props.row.preferredUsername }}</small>
</div>
</div>
</article>
</router-link>
</template>
</b-table-column>
<b-table-column field="domain" :label="$t('Domain')" searchable>
<template slot="searchable" slot-scope="props">
<b-input
v-model="props.filters.domain"
placeholder="Search..."
icon="magnify"
size="is-small"
/>
</template>
<template v-slot:default="props">
{{ props.row.domain }}
</template>
</b-table-column>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>{{ $t("No profile matches the filters") }}</p>
</div>
</section>
</template>
</b-table>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
import { LIST_PROFILES } from "../../graphql/actor";
import RouteName from "../../router/name";
import { LIST_GROUPS } from "@/graphql/group";
const PROFILES_PER_PAGE = 10;
@Component({
apollo: {
groups: {
query: LIST_GROUPS,
fetchPolicy: "cache-and-network",
variables() {
return {
preferredUsername: this.preferredUsername,
name: this.name,
domain: this.domain,
local: this.local,
suspended: this.suspended,
page: 1,
limit: PROFILES_PER_PAGE,
};
},
},
},
})
export default class GroupProfiles extends Vue {
page = 1;
preferredUsername = "";
name = "";
domain = "";
local = true;
suspended = false;
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
RouteName = RouteName;
async onPageChange(page: number) {
this.page = page;
await this.$apollo.queries.groups.fetchMore({
variables: {
preferredUsername: this.preferredUsername,
name: this.name,
domain: this.domain,
local: this.local,
suspended: this.suspended,
page: this.page,
limit: PROFILES_PER_PAGE,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult;
const newProfiles = fetchMoreResult.groups.elements;
return {
groups: {
__typename: previousResult.groups.__typename,
total: previousResult.groups.total,
elements: [...previousResult.groups.elements, ...newProfiles],
},
};
},
});
}
onFiltersChange({ preferredUsername, domain }: { preferredUsername: string; domain: string }) {
this.preferredUsername = preferredUsername;
this.domain = domain;
}
@Watch("domain")
domainNotLocal() {
this.local = this.domain === "";
}
}
</script>
<style lang="scss" scoped>
a.profile {
text-decoration: none;
}
</style>

View file

@ -91,6 +91,7 @@ const PROFILES_PER_PAGE = 10;
apollo: { apollo: {
persons: { persons: {
query: LIST_PROFILES, query: LIST_PROFILES,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
preferredUsername: this.preferredUsername, preferredUsername: this.preferredUsername,

View file

@ -103,6 +103,7 @@ const USERS_PER_PAGE = 10;
apollo: { apollo: {
users: { users: {
query: LIST_USERS, query: LIST_USERS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
email: this.email, email: this.email,

View file

@ -120,6 +120,7 @@ import { DELETE_COMMENT, UPDATE_COMMENT } from "@/graphql/comment";
apollo: { apollo: {
discussion: { discussion: {
query: GET_DISCUSSION, query: GET_DISCUSSION,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
slug: this.slug, slug: this.slug,

View file

@ -578,6 +578,7 @@ import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
apollo: { apollo: {
event: { event: {
query: FETCH_EVENT, query: FETCH_EVENT,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
uuid: this.uuid, uuid: this.uuid,
@ -592,6 +593,7 @@ import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
}, },
participations: { participations: {
query: EVENT_PERSON_PARTICIPATION, query: EVENT_PERSON_PARTICIPATION,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
eventId: this.event.id, eventId: this.event.id,

View file

@ -2,41 +2,64 @@
<section class="section container"> <section class="section container">
<h1>{{ $t("Create a new group") }}</h1> <h1>{{ $t("Create a new group") }}</h1>
<div> <b-message type="is-danger" v-for="(value, index) in errors" :key="index">
<b-field :label="$t('Group name')"> {{ value }}
<b-input aria-required="true" required v-model="group.preferredUsername" /> </b-message>
</b-field>
<b-field :label="$t('Group full name')"> <form @submit.prevent="createGroup">
<b-field :label="$t('Group display name')">
<b-input aria-required="true" required v-model="group.name" /> <b-input aria-required="true" required v-model="group.name" />
</b-field> </b-field>
<div class="field">
<label class="label">{{ $t("Federated Group Name") }}</label>
<div class="field-body">
<b-field>
<b-input aria-required="true" required expanded v-model="group.preferredUsername" />
<p class="control">
<span class="button is-static">@{{ host }}</span>
</p>
</b-field>
</div>
<p
v-html="
$t(
'This is like your federated username (<code>{username}</code>) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.',
{ username: usernameWithDomain(currentActor, true) }
)
"
/>
</div>
<b-field :label="$t('Description')"> <b-field :label="$t('Description')">
<b-input aria-required="true" required v-model="group.summary" type="textarea" /> <b-input v-model="group.summary" type="textarea" />
</b-field> </b-field>
<div> <div>
Avatar {{ $t("Avatar") }}
<picture-upload v-model="avatarFile" /> <picture-upload :textFallback="$t('Avatar')" v-model="avatarFile" />
</div> </div>
<div> <div>
Banner {{ $t("Banner") }}
<picture-upload v-model="avatarFile" /> <picture-upload :textFallback="$t('Banner')" v-model="bannerFile" />
</div> </div>
<button class="button is-primary" @click="createGroup()">{{ $t("Create my group") }}</button> <button class="button is-primary" native-type="submit">{{ $t("Create my group") }}</button>
</div> </form>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import { Group, IPerson } from "@/types/actor"; import { Group, IPerson, usernameWithDomain, MemberRole } from "@/types/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { CREATE_GROUP } from "@/graphql/group"; import { CREATE_GROUP } from "@/graphql/group";
import PictureUpload from "@/components/PictureUpload.vue"; import PictureUpload from "@/components/PictureUpload.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { mixins } from "vue-class-component";
import IdentityEditionMixin from "@/mixins/identityEdition";
import { convertToUsername } from "../../utils/username";
@Component({ @Component({
components: { components: {
@ -48,7 +71,7 @@ import RouteName from "../../router/name";
}, },
}, },
}) })
export default class CreateGroup extends Vue { export default class CreateGroup extends mixins(IdentityEditionMixin) {
currentActor!: IPerson; currentActor!: IPerson;
group = new Group(); group = new Group();
@ -57,19 +80,39 @@ export default class CreateGroup extends Vue {
bannerFile: File | null = null; bannerFile: File | null = null;
errors: string[] = [];
usernameWithDomain = usernameWithDomain;
async createGroup() { async createGroup() {
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: CREATE_GROUP, mutation: CREATE_GROUP,
variables: this.buildVariables(), variables: this.buildVariables(),
update: (store, { data: { createGroup } }) => { update: (store, { data: { createGroup } }) => {
// TODO: update group list cache const query = {
query: PERSON_MEMBERSHIPS,
variables: {
id: this.currentActor.id,
},
};
const membershipData = store.readQuery<{ person: IPerson }>(query);
if (!membershipData) return;
const person: IPerson = membershipData.person;
person.memberships.elements.push({
parent: createGroup,
role: MemberRole.ADMINISTRATOR,
actor: this.currentActor,
insertedAt: new Date().toString(),
updatedAt: new Date().toString(),
});
store.writeQuery({ ...query, data: { person } });
}, },
}); });
await this.$router.push({ await this.$router.push({
name: RouteName.GROUP, name: RouteName.GROUP,
params: { identityName: this.group.preferredUsername }, params: { preferredUsername: usernameWithDomain(this.group) },
}); });
this.$notifier.success( this.$notifier.success(
@ -82,6 +125,15 @@ export default class CreateGroup extends Vue {
} }
} }
get host(): string {
return window.location.hostname;
}
@Watch("group.name")
updateUsername(groupName: string): void {
this.group.preferredUsername = convertToUsername(groupName);
}
private buildVariables() { private buildVariables() {
let avatarObj = {}; let avatarObj = {};
let bannerObj = {}; let bannerObj = {};
@ -121,7 +173,18 @@ export default class CreateGroup extends Vue {
} }
private handleError(err: any) { private handleError(err: any) {
console.error(err); this.errors.push(
...err.graphQLErrors.map(({ message }: { message: string }) => this.convertMessage(message))
);
}
private convertMessage(message: string): string {
switch (message) {
case "A group with this name already exists":
return this.$t("A group with this name already exists") as string;
default:
return message;
}
} }
} }
</script> </script>

View file

@ -304,6 +304,7 @@ import addMinutes from "date-fns/addMinutes";
apollo: { apollo: {
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
name: this.preferredUsername, name: this.preferredUsername,
@ -312,6 +313,7 @@ import addMinutes from "date-fns/addMinutes";
}, },
person: { person: {
query: PERSON_MEMBERSHIPS, query: PERSON_MEMBERSHIPS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.currentActor.id, id: this.currentActor.id,

View file

@ -22,11 +22,15 @@ import { LIST_GROUPS } from "@/graphql/group";
import { Group, IGroup } from "@/types/actor"; import { Group, IGroup } from "@/types/actor";
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue"; import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { Paginate } from "@/types/paginate";
@Component({ @Component({
apollo: { apollo: {
groups: { groups: {
query: LIST_GROUPS, query: {
query: LIST_GROUPS,
fetchPolicy: "network-only",
},
}, },
}, },
components: { components: {
@ -34,7 +38,7 @@ import RouteName from "../../router/name";
}, },
}) })
export default class GroupList extends Vue { export default class GroupList extends Vue {
groups: { elements: IGroup[]; total: number } = { elements: [], total: 0 }; groups!: Paginate<IGroup>;
loading = true; loading = true;

View file

@ -176,7 +176,7 @@ import { IMember, MemberRole } from "../../types/actor/group.model";
apollo: { apollo: {
group: { group: {
query: GROUP_MEMBERS, query: GROUP_MEMBERS,
// fetchPolicy: "network-only", fetchPolicy: "network-only",
variables() { variables() {
return { return {
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,

View file

@ -91,7 +91,10 @@
:value="currentAddress" :value="currentAddress"
/> />
<b-button native-type="submit" type="is-primary">{{ $t("Update group") }}</b-button> <div class="buttons">
<b-button native-type="submit" type="is-primary">{{ $t("Update group") }}</b-button>
<b-button @click="confirmDeleteGroup" type="is-danger">{{ $t("Delete group") }}</b-button>
</div>
</form> </form>
</section> </section>
</div> </div>
@ -100,7 +103,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { FETCH_GROUP, UPDATE_GROUP } from "../../graphql/group"; import { FETCH_GROUP, UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
import { IMember, Group } from "../../types/actor/group.model"; import { IMember, Group } from "../../types/actor/group.model";
@ -111,6 +114,7 @@ import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.
apollo: { apollo: {
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,
@ -148,15 +152,41 @@ export default class GroupSettings extends Vue {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
delete variables.__typename; delete variables.__typename;
// eslint-disable-next-line if (variables.physicalAddress) {
// @ts-ignore // eslint-disable-next-line
delete variables.physicalAddress.__typename; // @ts-ignore
delete variables.physicalAddress.__typename;
}
await this.$apollo.mutate<{ updateGroup: IGroup }>({ await this.$apollo.mutate<{ updateGroup: IGroup }>({
mutation: UPDATE_GROUP, mutation: UPDATE_GROUP,
variables, variables,
}); });
} }
confirmDeleteGroup() {
this.$buefy.dialog.confirm({
title: this.$t("Delete group") as string,
message: this.$t(
"Are you sure you want to <b>completely delete</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
) as string,
confirmText: this.$t("Delete group") as string,
cancelText: this.$t("Cancel") as string,
type: "is-danger",
hasIcon: true,
onConfirm: () => this.deleteGroup(),
});
}
async deleteGroup() {
await this.$apollo.mutate<{ deleteGroup: IGroup }>({
mutation: DELETE_GROUP,
variables: {
groupId: this.group.id,
},
});
return this.$router.push({ name: RouteName.MY_GROUPS });
}
async copyURL() { async copyURL() {
await window.navigator.clipboard.writeText(this.group.url); await window.navigator.clipboard.writeText(this.group.url);
this.showCopiedTooltip = true; this.showCopiedTooltip = true;

View file

@ -85,7 +85,7 @@ export default class MyEvents extends Vue {
get memberships() { get memberships() {
if (!this.membershipsPages) return []; if (!this.membershipsPages) return [];
return this.membershipsPages.elements.filter( return this.membershipsPages.elements.filter(
(member: IMember) => member.role !== MemberRole.INVITED (member: IMember) => ![MemberRole.INVITED, MemberRole.REJECTED].includes(member.role)
); );
} }
} }

View file

@ -170,6 +170,7 @@ import { displayNameAndUsername } from "../../types/actor";
}, },
apollo: { apollo: {
actionLogs: { actionLogs: {
fetchPolicy: "cache-and-network",
query: LOGS, query: LOGS,
}, },
}, },

View file

@ -232,6 +232,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
report: { report: {
query: REPORT, query: REPORT,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.reportId, id: this.reportId,

View file

@ -57,7 +57,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
reports: { reports: {
query: REPORTS, query: REPORTS,
fetchPolicy: "no-cache", fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
status: this.filterReports, status: this.filterReports,

View file

@ -98,6 +98,7 @@ import RouteName from "../../router/name";
config: CONFIG, config: CONFIG,
post: { post: {
query: FETCH_POST, query: FETCH_POST,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
slug: this.slug, slug: this.slug,

View file

@ -57,6 +57,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
group: { group: {
query: FETCH_GROUP_POSTS, query: FETCH_GROUP_POSTS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
preferredUsername: this.preferredUsername, preferredUsername: this.preferredUsername,

View file

@ -58,6 +58,7 @@ import Tag from "../../components/Tag.vue";
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
memberships: { memberships: {
query: PERSON_MEMBERSHIPS, query: PERSON_MEMBERSHIPS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.currentActor.id, id: this.currentActor.id,
@ -70,6 +71,7 @@ import Tag from "../../components/Tag.vue";
}, },
post: { post: {
query: FETCH_POST, query: FETCH_POST,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
slug: this.slug, slug: this.slug,

View file

@ -218,6 +218,7 @@ import ResourceSelector from "../../components/Resource/ResourceSelector.vue";
apollo: { apollo: {
resource: { resource: {
query: GET_RESOURCE, query: GET_RESOURCE,
fetchPolicy: "cache-and-network",
variables() { variables() {
let path = Array.isArray(this.$route.params.path) let path = Array.isArray(this.$route.params.path)
? this.$route.params.path.join("/") ? this.$route.params.path.join("/")

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="section container"> <div class="section container">
<h1 class="title">{{ $t("Explore") }}</h1> <h1 class="title">{{ $t("Explore") }}</h1>
<section v-if="actualTag"> <section v-if="tag">
<i18n path="Events tagged with {tag}"> <i18n path="Events tagged with {tag}">
<b-tag slot="tag" type="is-light">{{ $t("#{tag}", { tag: actualTag }) }}</b-tag> <b-tag slot="tag" type="is-light">{{ $t("#{tag}", { tag }) }}</b-tag>
</i18n> </i18n>
</section> </section>
<section class="hero is-light" v-else> <section class="hero is-light" v-else>
@ -49,7 +49,7 @@
</form> </form>
</div> </div>
</section> </section>
<section class="events-featured" v-if="!actualTag && searchEvents.initial"> <section class="events-featured" v-if="!tag && searchEvents.initial">
<b-loading :active.sync="$apollo.loading"></b-loading> <b-loading :active.sync="$apollo.loading"></b-loading>
<h2 class="title">{{ $t("Featured events") }}</h2> <h2 class="title">{{ $t("Featured events") }}</h2>
<div v-if="events.length > 0" class="columns is-multiline"> <div v-if="events.length > 0" class="columns is-multiline">
@ -163,10 +163,11 @@ const tabsName: { events: number; groups: number } = {
events: FETCH_EVENTS, events: FETCH_EVENTS,
searchEvents: { searchEvents: {
query: SEARCH_EVENTS, query: SEARCH_EVENTS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
term: this.search, term: this.search,
tags: this.actualTag, tags: this.tag,
location: this.geohash, location: this.geohash,
beginsOn: this.start, beginsOn: this.start,
endsOn: this.end, endsOn: this.end,
@ -175,11 +176,12 @@ const tabsName: { events: number; groups: number } = {
}, },
debounce: 300, debounce: 300,
skip() { skip() {
return !this.search && !this.actualTag && !this.geohash && this.end === null; return !this.search && !this.tag && !this.geohash && this.end === null;
}, },
}, },
searchGroups: { searchGroups: {
query: SEARCH_GROUPS, query: SEARCH_GROUPS,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
term: this.search, term: this.search,
@ -213,7 +215,6 @@ export default class Search extends Vue {
activeTab: SearchTabs = tabsName[this.$route.query.searchType as "events" | "groups"] || 0; activeTab: SearchTabs = tabsName[this.$route.query.searchType as "events" | "groups"] || 0;
location: IAddress = new Address(); location: IAddress = new Address();
actualTag: string = this.tag;
options: ISearchTimeOption[] = [ options: ISearchTimeOption[] = [
{ {

View file

@ -40,6 +40,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
todo: { todo: {
query: GET_TODO, query: GET_TODO,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.$route.params.todoId, id: this.$route.params.todoId,

View file

@ -56,6 +56,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
todoList: { todoList: {
query: FETCH_TODO_LIST, query: FETCH_TODO_LIST,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
id: this.$route.params.id, id: this.$route.params.id,

View file

@ -55,6 +55,7 @@ import RouteName from "../../router/name";
apollo: { apollo: {
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() { variables() {
return { return {
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,

View file

@ -96,20 +96,26 @@ defmodule Mobilizon.Federation.ActivityPub do
{:existing, entity} -> {:existing, entity} ->
Logger.debug("Entity is already existing") Logger.debug("Entity is already existing")
entity = res =
if force_fetch and not are_same_origin?(url, Endpoint.url()) do if force_fetch and not are_same_origin?(url, Endpoint.url()) do
Logger.debug("Entity is external and we want a force fetch") Logger.debug("Entity is external and we want a force fetch")
with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do case Fetcher.fetch_and_update(url, options) do
entity {:ok, _activity, entity} ->
{:ok, entity}
{:error, "Gone"} ->
{:error, "Gone", entity}
end end
else else
entity {:ok, entity}
end end
Logger.debug("Going to preload an existing entity") with {:ok, entity} <- res do
Logger.debug("Going to preload an existing entity")
Preloader.maybe_preload(entity) Preloader.maybe_preload(entity)
end
e -> e ->
Logger.warn("Something failed while fetching url #{inspect(e)}") Logger.warn("Something failed while fetching url #{inspect(e)}")
@ -333,9 +339,9 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
def delete(object, actor, local \\ true) do def delete(object, actor, local \\ true, additional \\ %{}) do
with {:ok, activity_data, actor, object} <- with {:ok, activity_data, actor, object} <-
Managable.delete(object, actor, local), Managable.delete(object, actor, local, additional),
group <- Ownable.group_actor(object), group <- Ownable.group_actor(object),
:ok <- check_for_actor_key_rotation(actor), :ok <- check_for_actor_key_rotation(actor),
{:ok, activity} <- create_activity(activity_data, local), {:ok, activity} <- create_activity(activity_data, local),
@ -417,12 +423,14 @@ defmodule Mobilizon.Federation.ActivityPub do
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url}, %Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
%Actor{id: actor_id, url: actor_url}, %Actor{id: actor_id, url: actor_url},
local, local,
_additional additional
) do ) do
with {:member, {:ok, %Member{id: member_id} = member}} <- with {:member, {:ok, %Member{id: member_id} = member}} <-
{:member, Actors.get_member(actor_id, group_id)}, {:member, Actors.get_member(actor_id, group_id)},
{:is_only_admin, false} <- {:is_not_only_admin, true} <-
{:is_only_admin, Actors.is_only_administrator?(member_id, group_id)}, {:is_not_only_admin,
Map.get(additional, :force_member_removal, false) ||
!Actors.is_only_administrator?(member_id, group_id)},
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)}, {:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
leave_data <- %{ leave_data <- %{
"to" => [group_members_url], "to" => [group_members_url],
@ -639,6 +647,19 @@ defmodule Mobilizon.Federation.ActivityPub do
end) end)
end end
defp convert_followers_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
case Actors.get_group_by_followers_url(recipient) do
%Actor{} = group ->
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
follower_actors ++ Actors.list_external_followers_for_actor(group)}
_ ->
acc
end
end)
end
# @spec is_announce_activity?(Activity.t()) :: boolean # @spec is_announce_activity?(Activity.t()) :: boolean
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true # defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
# defp is_announce_activity?(_), do: false # defp is_announce_activity?(_), do: false
@ -661,13 +682,7 @@ defmodule Mobilizon.Federation.ActivityPub do
Relay.publish(activity) Relay.publish(activity)
end end
{recipients, followers} = {recipients, followers} = convert_followers_in_recipients(recipients)
if actor.followers_url in activity.recipients do
{Enum.filter(recipients, fn recipient -> recipient != actor.followers_url end),
Actors.list_external_followers_for_actor(actor)}
else
{recipients, []}
end
{recipients, members} = convert_members_in_recipients(recipients) {recipients, members} = convert_members_in_recipients(recipients)
@ -858,7 +873,7 @@ defmodule Mobilizon.Federation.ActivityPub do
) do ) do
with %Actor{} = inviter <- Actors.get_actor(invited_by_id), with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
%Actor{url: actor_url} <- Actors.get_actor(actor_id), %Actor{url: actor_url} <- Actors.get_actor(actor_id),
{:ok, %Member{url: member_url, id: member_id} = member} <- {:ok, %Member{id: member_id} = member} <-
Actors.update_member(member, %{role: :member}), Actors.update_member(member, %{role: :member}),
accept_data <- %{ accept_data <- %{
"type" => "Accept", "type" => "Accept",
@ -866,7 +881,7 @@ defmodule Mobilizon.Federation.ActivityPub do
"to" => [inviter.url, member.parent.members_url], "to" => [inviter.url, member.parent.members_url],
"cc" => [member.parent.url], "cc" => [member.parent.url],
"actor" => actor_url, "actor" => actor_url,
"object" => member_url, "object" => Convertible.model_to_as(member),
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}" "id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
} do } do
{:ok, member, accept_data} {:ok, member, accept_data}

View file

@ -27,6 +27,12 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <- {:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
ActivityPubClient.get(client, url) do ActivityPubClient.get(client, url) do
{:ok, data} {:ok, data}
else
{:ok, %Tesla.Env{status: 410}} ->
{:error, "Gone"}
{:ok, %Tesla.Env{} = res} ->
{:error, res}
end end
end end
@ -47,6 +53,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
{:origin_check, false} -> {:origin_check, false} ->
Logger.warn("Object origin check failed") Logger.warn("Object origin check failed")
{:error, "Object origin check failed"} {:error, "Object origin check failed"}
{:error, err} ->
{:error, err}
end end
end end
@ -67,6 +76,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
{:origin_check, false} -> {:origin_check, false} ->
Logger.warn("Object origin check failed") Logger.warn("Object origin check failed")
{:error, "Object origin check failed"} {:error, "Object origin check failed"}
{:error, err} ->
{:error, err}
end end
end end
end end

View file

@ -3,11 +3,33 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Module that provides functions to explore and fetch collections on a group Module that provides functions to explore and fetch collections on a group
""" """
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier} alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier}
require Logger require Logger
def refresh_profile(%Actor{domain: nil}), do: {:error, "Can only refresh remote actors"}
def refresh_profile(%Actor{type: :Group, url: url, id: group_id} = group) do
on_behalf_of =
case Actors.get_single_group_member_actor(group_id) do
%Actor{} = member_actor ->
member_actor
_ ->
Relay.get_actor()
end
with :ok <- fetch_group(url, on_behalf_of) do
{:ok, group}
end
end
def refresh_profile(%Actor{type: :Person, url: url}) do
ActivityPub.make_actor_from_url(url)
end
@spec fetch_group(String.t(), Actor.t()) :: :ok @spec fetch_group(String.t(), Actor.t()) :: :ok
def fetch_group(group_url, %Actor{} = on_behalf_of) do def fetch_group(group_url, %Actor{} = on_behalf_of) do
with {:ok, with {:ok,
@ -20,14 +42,15 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
discussions_url: discussions_url, discussions_url: discussions_url,
events_url: events_url events_url: events_url
}} <- }} <-
ActivityPub.get_or_fetch_actor_by_url(group_url) do ActivityPub.make_actor_from_url(group_url),
fetch_collection(outbox_url, on_behalf_of) :ok <- fetch_collection(outbox_url, on_behalf_of),
fetch_collection(members_url, on_behalf_of) :ok <- fetch_collection(members_url, on_behalf_of),
fetch_collection(resources_url, on_behalf_of) :ok <- fetch_collection(resources_url, on_behalf_of),
fetch_collection(posts_url, on_behalf_of) :ok <- fetch_collection(posts_url, on_behalf_of),
fetch_collection(todos_url, on_behalf_of) :ok <- fetch_collection(todos_url, on_behalf_of),
fetch_collection(discussions_url, on_behalf_of) :ok <- fetch_collection(discussions_url, on_behalf_of),
fetch_collection(events_url, on_behalf_of) :ok <- fetch_collection(events_url, on_behalf_of) do
:ok
end end
end end
@ -37,9 +60,10 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Logger.debug("Fetching and preparing collection from url") Logger.debug("Fetching and preparing collection from url")
Logger.debug(inspect(collection_url)) Logger.debug(inspect(collection_url))
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of),
Logger.debug("Fetch ok, passing to process_collection") :ok <- Logger.debug("Fetch ok, passing to process_collection"),
process_collection(data, on_behalf_of) :ok <- process_collection(data, on_behalf_of) do
:ok
end end
end end
@ -68,6 +92,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Logger.debug(inspect(items)) Logger.debug(inspect(items))
Enum.each(items, &handling_element/1) Enum.each(items, &handling_element/1)
:ok
end end
defp process_collection(%{"type" => "OrderedCollection", "first" => first}, on_behalf_of) defp process_collection(%{"type" => "OrderedCollection", "first" => first}, on_behalf_of)
@ -84,7 +109,15 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
end end
end end
defp process_collection(_, _), do: :error
defp handling_element(data) when is_map(data) do defp handling_element(data) when is_map(data) do
id = Map.get(data, "id")
if id do
Mobilizon.Tombstone.delete_uri_tombstone(id)
end
activity = %{ activity = %{
"type" => "Create", "type" => "Create",
"to" => data["to"], "to" => data["to"],

View file

@ -131,15 +131,18 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Logger.info("Handle incoming to create a member") Logger.info("Handle incoming to create a member")
with object_data when is_map(object_data) <- with object_data when is_map(object_data) <-
object |> Converter.Member.as_to_model_data(), object |> Converter.Member.as_to_model_data() do
{:existing_member, nil} <- with {:existing_member, nil} <-
{:existing_member, Actors.get_member_by_url(object_data.url)}, {:existing_member, Actors.get_member_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Member{} = member} <- {:ok, %Activity{} = activity, %Member{} = member} <-
ActivityPub.join_group(object_data, false) do ActivityPub.join_group(object_data, false) do
{:ok, activity, member} {:ok, activity, member}
else else
{:existing_member, %Member{} = member} -> {:existing_member, %Member{} = member} ->
{:ok, nil, member} {:ok, %Member{} = member} = Actors.update_member(member, object_data)
{:ok, nil, member}
end
end end
end end
@ -502,7 +505,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with actor_url <- Utils.get_actor(data), with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
object_id <- Utils.get_url(object), object_id <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id), {:error, "Gone", object} <- ActivityPub.fetch_object_from_url(object_id, force: true),
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) || Utils.origin_check_from_id?(actor_url, object_id) ||
@ -515,7 +518,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:error :error
e -> e ->
Logger.debug(inspect(e)) Logger.error(inspect(e))
:error :error
end end
end end

View file

@ -38,29 +38,55 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
end end
@public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
def delete( def delete(
%Actor{followers_url: followers_url, url: target_actor_url} = target_actor, %Actor{
%Actor{url: actor_url} = actor, followers_url: followers_url,
local members_url: members_url,
url: target_actor_url,
type: type,
domain: domain
} = target_actor,
%Actor{url: actor_url, id: author_id} = actor,
_local,
additionnal
) do ) do
to = [@public_ap, followers_url]
{to, cc} =
if type == :Group do
{to ++ [members_url], [target_actor_url]}
else
{to, []}
end
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
"actor" => actor_url, "actor" => actor_url,
"object" => Convertible.model_to_as(target_actor), "object" => Convertible.model_to_as(target_actor),
"id" => target_actor_url <> "/delete", "id" => target_actor_url <> "/delete",
"to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"] "to" => to,
"cc" => cc
} }
# We completely delete the actor if activity is remote suspension = Map.get(additionnal, :suspension, false)
with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do
with {:ok, %Oban.Job{}} <-
Actors.delete_actor(target_actor,
# We completely delete the actor if the actor is remote
reserve_username: is_nil(domain),
suspension: suspension,
author_id: author_id
) do
{:ok, activity_data, actor, target_actor} {:ok, activity_data, actor, target_actor}
end end
end end
def actor(%Actor{} = actor), do: actor def actor(%Actor{} = actor), do: actor
def group_actor(%Actor{} = _actor), do: nil def group_actor(%Actor{} = actor), do: actor
defp prepare_args_for_actor(args) do defp prepare_args_for_actor(args) do
with preferred_username <- with preferred_username <-

View file

@ -53,8 +53,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
@impl Entity @impl Entity
@spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()} @spec delete(Comment.t(), Actor.t(), boolean, map()) :: {:ok, Comment.t()}
def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do def delete(
%Comment{url: url, id: comment_id},
%Actor{} = actor,
_local,
options \\ %{}
) do
comment = Discussions.get_comment_with_preload(comment_id)
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
"actor" => actor.url, "actor" => actor.url,
@ -63,16 +70,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
"to" => ["https://www.w3.org/ns/activitystreams#Public"] "to" => ["https://www.w3.org/ns/activitystreams#Public"]
} }
force_deletion = Map.get(options, :force, false)
with audience <- with audience <-
Audience.calculate_to_and_cc_from_mentions(comment), Audience.calculate_to_and_cc_from_mentions(comment),
{:ok, %Comment{} = comment} <- Discussions.delete_comment(comment), {:ok, %Comment{} = updated_comment} <-
# Preload to be sure Discussions.delete_comment(comment, force: force_deletion),
%Comment{} = comment <- Discussions.get_comment_with_preload(comment.id),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
{:ok, %Tombstone{} = _tombstone} <- {:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
Share.delete_all_by_uri(comment.url) Share.delete_all_by_uri(comment.url)
{:ok, Map.merge(activity_data, audience), actor, comment} {:ok, Map.merge(activity_data, audience), actor, updated_comment}
end end
end end

View file

@ -5,9 +5,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.Audience alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity} alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Storage.Repo
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
require Logger require Logger
@ -65,28 +64,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
@impl Entity @impl Entity
@spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()} @spec delete(Discussion.t(), Actor.t(), boolean, map()) :: {:ok, Discussion.t()}
def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do def delete(
stream = %Discussion{actor: group, url: url} = discussion,
discussion.comments %Actor{} = actor,
|> Enum.map( _local,
&Repo.preload(&1, [ _additionnal
:actor, ) do
:attributed_to, with {:ok, _} <- Discussions.delete_discussion(discussion) do
:in_reply_to_comment,
:mentions,
:origin_comment,
:discussion,
:tags,
:replies
])
)
|> Enum.map(&Map.put(&1, :event, nil))
|> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end)
Stream.run(stream)
with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do
# This is just fake # This is just fake
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",

View file

@ -35,7 +35,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) :: @callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()} {:ok, t(), ActivityStream.t()}
@callback delete(struct :: t(), Actor.t(), local :: boolean()) :: @callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) ::
{:ok, ActivityStream.t(), Actor.t(), t()} {:ok, ActivityStream.t(), Actor.t(), t()}
end end
@ -50,10 +50,10 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
""" """
def update(entity, attrs, additionnal) def update(entity, attrs, additionnal)
@spec delete(Entity.t(), Actor.t(), boolean()) :: @spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
{:ok, ActivityStream.t(), Actor.t(), Entity.t()} {:ok, ActivityStream.t(), Actor.t(), Entity.t()}
@doc "Deletes an entity and returns the activitystream representation for it" @doc "Deletes an entity and returns the activitystream representation for it"
def delete(entity, actor, local) def delete(entity, actor, local, additionnal)
end end
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
@ -68,7 +68,7 @@ end
defimpl Managable, for: Event do defimpl Managable, for: Event do
defdelegate update(entity, attrs, additionnal), to: Events defdelegate update(entity, attrs, additionnal), to: Events
defdelegate delete(entity, actor, local), to: Events defdelegate delete(entity, actor, local, additionnal), to: Events
end end
defimpl Ownable, for: Event do defimpl Ownable, for: Event do
@ -78,7 +78,7 @@ end
defimpl Managable, for: Comment do defimpl Managable, for: Comment do
defdelegate update(entity, attrs, additionnal), to: Comments defdelegate update(entity, attrs, additionnal), to: Comments
defdelegate delete(entity, actor, local), to: Comments defdelegate delete(entity, actor, local, additionnal), to: Comments
end end
defimpl Ownable, for: Comment do defimpl Ownable, for: Comment do
@ -88,7 +88,7 @@ end
defimpl Managable, for: Post do defimpl Managable, for: Post do
defdelegate update(entity, attrs, additionnal), to: Posts defdelegate update(entity, attrs, additionnal), to: Posts
defdelegate delete(entity, actor, local), to: Posts defdelegate delete(entity, actor, local, additionnal), to: Posts
end end
defimpl Ownable, for: Post do defimpl Ownable, for: Post do
@ -98,7 +98,7 @@ end
defimpl Managable, for: Actor do defimpl Managable, for: Actor do
defdelegate update(entity, attrs, additionnal), to: Actors defdelegate update(entity, attrs, additionnal), to: Actors
defdelegate delete(entity, actor, local), to: Actors defdelegate delete(entity, actor, local, additionnal), to: Actors
end end
defimpl Ownable, for: Actor do defimpl Ownable, for: Actor do
@ -108,7 +108,7 @@ end
defimpl Managable, for: TodoList do defimpl Managable, for: TodoList do
defdelegate update(entity, attrs, additionnal), to: TodoLists defdelegate update(entity, attrs, additionnal), to: TodoLists
defdelegate delete(entity, actor, local), to: TodoLists defdelegate delete(entity, actor, local, additionnal), to: TodoLists
end end
defimpl Ownable, for: TodoList do defimpl Ownable, for: TodoList do
@ -118,7 +118,7 @@ end
defimpl Managable, for: Todo do defimpl Managable, for: Todo do
defdelegate update(entity, attrs, additionnal), to: Todos defdelegate update(entity, attrs, additionnal), to: Todos
defdelegate delete(entity, actor, local), to: Todos defdelegate delete(entity, actor, local, additionnal), to: Todos
end end
defimpl Ownable, for: Todo do defimpl Ownable, for: Todo do
@ -128,7 +128,7 @@ end
defimpl Managable, for: Resource do defimpl Managable, for: Resource do
defdelegate update(entity, attrs, additionnal), to: Resources defdelegate update(entity, attrs, additionnal), to: Resources
defdelegate delete(entity, actor, local), to: Resources defdelegate delete(entity, actor, local, additionnal), to: Resources
end end
defimpl Ownable, for: Resource do defimpl Ownable, for: Resource do
@ -138,7 +138,7 @@ end
defimpl Managable, for: Discussion do defimpl Managable, for: Discussion do
defdelegate update(entity, attrs, additionnal), to: Discussions defdelegate update(entity, attrs, additionnal), to: Discussions
defdelegate delete(entity, actor, local), to: Discussions defdelegate delete(entity, actor, local, additionnal), to: Discussions
end end
defimpl Ownable, for: Discussion do defimpl Ownable, for: Discussion do
@ -153,5 +153,5 @@ end
defimpl Managable, for: Member do defimpl Managable, for: Member do
defdelegate update(entity, attrs, additionnal), to: Members defdelegate update(entity, attrs, additionnal), to: Members
defdelegate delete(entity, actor, local), to: Members defdelegate delete(entity, actor, local, additionnal), to: Members
end end

View file

@ -53,8 +53,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
@impl Entity @impl Entity
@spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()} @spec delete(Event.t(), Actor.t(), boolean, map()) :: {:ok, Event.t()}
def delete(%Event{url: url} = event, %Actor{} = actor, _local) do def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
"actor" => actor.url, "actor" => actor.url,

View file

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
require Logger require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2]
@ -38,8 +39,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
end end
end end
# Delete member is not used, see ActivityPub.leave/4 and ActivityPub.remove/5 instead # Used only when a group is suspended
def delete(_, _, _), do: :error def delete(
%Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member,
%Actor{},
local,
_additionnal
) do
Logger.debug("Deleting a member")
ActivityPub.leave(group, actor, local, %{force_member_removal: true})
end
def actor(%Member{actor_id: actor_id}), def actor(%Member{actor_id: actor_id}),
do: Actors.get_actor(actor_id) do: Actors.get_actor(actor_id)

View file

@ -69,7 +69,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
attributed_to: %Actor{url: group_url} attributed_to: %Actor{url: group_url}
} = post, } = post,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
_local _local,
_additionnal
) do ) do
activity_data = %{ activity_data = %{
"actor" => actor_url, "actor" => actor_url,

View file

@ -131,7 +131,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
def delete( def delete(
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
_local _local,
_additionnal
) do ) do
Logger.debug("Building Delete Resource activity") Logger.debug("Building Delete Resource activity")

View file

@ -40,19 +40,20 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
end end
@impl Entity @impl Entity
@spec delete(TodoList.t(), Actor.t(), boolean()) :: @spec delete(TodoList.t(), Actor.t(), boolean(), map()) ::
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()} {:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
def delete( def delete(
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list, %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
_local _local,
_additionnal
) do ) do
Logger.debug("Building Delete TodoList activity") Logger.debug("Building Delete TodoList activity")
activity_data = %{ activity_data = %{
"actor" => actor_url, "actor" => actor_url,
"type" => "Delete", "type" => "Delete",
"object" => Convertible.model_to_as(url), "object" => Convertible.model_to_as(todo_list),
"id" => url <> "/delete", "id" => url <> "/delete",
"to" => [group_url] "to" => [group_url]
} }

View file

@ -44,11 +44,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
@impl Entity @impl Entity
@spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()} @spec delete(Todo.t(), Actor.t(), boolean(), map()) ::
{:ok, ActivityStream.t(), Actor.t(), Todo.t()}
def delete( def delete(
%Todo{url: url, creator: %Actor{url: group_url}} = todo, %Todo{url: url, creator: %Actor{url: group_url}} = todo,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
_local _local,
_additionnal
) do ) do
Logger.debug("Building Delete Todo activity") Logger.debug("Building Delete Todo activity")

View file

@ -143,7 +143,9 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
do_maybe_relay_if_group_activity(object, attributed_to_url) do_maybe_relay_if_group_activity(object, attributed_to_url)
end end
def maybe_relay_if_group_activity(_, _), do: :ok def maybe_relay_if_group_activity(_activity, _attributedTo) do
:ok
end
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to), defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
do: do_maybe_relay_if_group_activity(object, hd(attributed_to)) do: do_maybe_relay_if_group_activity(object, hd(attributed_to))

View file

@ -135,6 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
def model_to_as(%CommentModel{} = comment) do def model_to_as(%CommentModel{} = comment) do
Convertible.model_to_as(%TombstoneModel{ Convertible.model_to_as(%TombstoneModel{
uri: comment.url, uri: comment.url,
actor: comment.actor,
inserted_at: comment.deleted_at inserted_at: comment.deleted_at
}) })
end end

View file

@ -39,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
"actor" => discussion.creator.url, "actor" => discussion.creator.url,
"attributedTo" => discussion.actor.url, "attributedTo" => discussion.actor.url,
"id" => discussion.url, "id" => discussion.url,
"publishedAt" => discussion.inserted_at,
"context" => discussion.url "context" => discussion.url
} }
end end

View file

@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
%{ %{
"type" => "Tombstone", "type" => "Tombstone",
"id" => tombstone.uri, "id" => tombstone.uri,
"actor" => tombstone.actor.url, "actor" => if(tombstone.actor, do: tombstone.actor.url, else: nil),
"deleted" => tombstone.inserted_at "deleted" => tombstone.inserted_at
} }
end end

View file

@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.API.Groups do
with preferred_username <- with preferred_username <-
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
{:existing_group, nil} <- {:existing_group, nil} <-
{:existing_group, Actors.get_local_group_by_title(preferred_username)}, {:existing_group, Actors.get_local_actor_by_name(preferred_username)},
args <- args |> Map.put(:type, :Group), args <- args |> Map.put(:type, :Group),
{:ok, %Activity{} = activity, %Actor{} = group} <- {:ok, %Activity{} = activity, %Actor{} = group} <-
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do

View file

@ -0,0 +1,104 @@
defmodule Mobilizon.GraphQL.Resolvers.Actor do
@moduledoc """
Handles the group-related GraphQL calls.
"""
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Admin, Users}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Refresher
alias Mobilizon.Users.User
require Logger
def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Actors.get_actor(id) do
%Actor{domain: domain} = actor when not is_nil(domain) ->
Refresher.refresh_profile(actor)
{:ok, actor}
%Actor{} ->
{:error, "Only remote actors may be refreshed"}
_ ->
{:error, "No actor found with this ID"}
end
end
def suspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: false} = actor <- Actors.get_actor_with_preload(id) do
case actor do
# Suspend a group on this instance
%Actor{type: :Group, domain: nil} ->
Logger.debug("We're suspending a group on this very instance")
ActivityPub.delete(actor, moderator_actor, true, %{suspension: true})
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
# Delete a remote actor
%Actor{domain: domain} when not is_nil(domain) ->
Logger.debug("We're just deleting a remote instance")
Actors.delete_actor(actor, suspension: true)
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
%Actor{domain: nil} ->
{:error, "No remote profile found with this ID"}
end
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
%Actor{suspended: true} ->
{:error, "Actor already suspended"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def suspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can suspend a profile"}
end
def unsuspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: true} = actor <-
Actors.get_actor_with_preload(id, true),
{:delete_tombstones, {_, nil}} <-
{:delete_tombstones, Mobilizon.Tombstone.delete_actor_tombstones(id)},
{:ok, %Actor{} = actor} <- Actors.update_actor(actor, %{suspended: false}),
{:ok, %Actor{} = actor} <- refresh_if_remote(actor),
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
{:ok, actor}
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
nil ->
{:error, "No remote profile found with this ID"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def unsuspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can unsuspend a profile"}
end
@spec refresh_if_remote(Actor.t()) :: {:ok, Actor.t()}
defp refresh_if_remote(%Actor{domain: nil} = actor), do: {:ok, actor}
defp refresh_if_remote(%Actor{} = actor), do: Refresher.refresh_profile(actor)
end

View file

@ -20,8 +20,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, Discussions.find_discussions_for_actor(group_id)} {:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(group_id) do
{:ok, Discussions.find_discussions_for_actor(group)}
else else
{:member, false} -> {:member, false} ->
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
@ -174,6 +175,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
{:ok, _activity, %Discussion{} = discussion} <- {:ok, _activity, %Discussion{} = discussion} <-
ActivityPub.delete(discussion, actor) do ActivityPub.delete(discussion, actor) do
{:ok, discussion} {:ok, discussion}
else
{:no_discussion, _} ->
{:error, "No discussion with ID #{discussion_id}"}
{:member, _} ->
{:error, "You are not a member of the group the discussion belongs to"}
end end
end end
end end

View file

@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
Handles the group-related GraphQL calls. Handles the group-related GraphQL calls.
""" """
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Events, Users} alias Mobilizon.{Actors, Events, Users}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
@ -54,13 +55,46 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
end end
end end
@doc """
Get a group
"""
def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
with %Actor{type: :Group, suspended: suspended} = actor <-
Actors.get_actor_with_preload(id, true),
true <- suspended == false or is_moderator(role) do
{:ok, actor}
else
_ ->
{:error, "Group with ID #{id} not found"}
end
end
@doc """ @doc """
Lists all groups Lists all groups
""" """
def list_groups(_parent, %{page: page, limit: limit}, _resolution) do def list_groups(
{:ok, Actors.list_groups(page, limit)} _parent,
%{
preferred_username: preferred_username,
name: name,
domain: domain,
local: local,
suspended: suspended,
page: page,
limit: limit
},
%{
context: %{current_user: %User{role: role}}
}
)
when is_moderator(role) do
{:ok,
Actors.list_actors(:Group, preferred_username, name, domain, local, suspended, page, limit)}
end end
def list_groups(_parent, _args, _resolution),
do: {:error, "You may not list groups unless moderator."}
@doc """ @doc """
Create a new group. The creator is automatically added as admin Create a new group. The creator is automatically added as admin
""" """
@ -127,28 +161,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
""" """
def delete_group( def delete_group(
_parent, _parent,
%{group_id: group_id, actor_id: actor_id}, %{group_id: group_id},
%{ %{
context: %{ context: %{
current_user: user current_user: user
} }
} }
) do ) do
with {actor_id, ""} <- Integer.parse(actor_id), with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
{group_id, ""} <- Integer.parse(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id), {:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)}, {:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
group <- Actors.delete_group!(group) do {:ok, _activity, group} <- ActivityPub.delete(group, actor, true) do
{:ok, %{id: group.id}} {:ok, %{id: group.id}}
else else
{:error, :group_not_found} -> {:error, :group_not_found} ->
{:error, "Group not found"} {:error, "Group not found"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} -> {:error, :member_not_found} ->
{:error, "Actor id is not a member of this group"} {:error, "Actor id is not a member of this group"}
@ -231,7 +260,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:group, nil} -> {:group, nil} ->
{:error, "Group not found"} {:error, "Group not found"}
{:is_only_admin, true} -> {:is_not_only_admin, false} ->
{:error, "You can't leave this group because you are the only administrator"} {:error, "You can't leave this group because you are the only administrator"}
end end
end end
@ -245,12 +274,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
_args, _args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_user: %User{role: user_role} = user
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do {:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
# TODO : Handle public / restricted to group members events # TODO : Handle public / restricted to group members events
{:ok, Events.list_organized_events_for_group(group)} {:ok, Events.list_organized_events_for_group(group)}
else else

View file

@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
Handles the member-related GraphQL calls Handles the member-related GraphQL calls
""" """
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Users} alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
@ -19,11 +20,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
%{page: page, limit: limit, roles: roles}, %{page: page, limit: limit, roles: roles},
%{ %{
context: %{current_user: %User{} = user} context: %{current_user: %User{role: user_role} = user}
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do {:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
roles = roles =
case roles do case roles do
"" -> "" ->
@ -167,9 +169,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:error, :member_not_found} -> {:error, :member_not_found} ->
true true
err -> _err ->
require Logger
Logger.error(inspect(err))
false false
end end
end end

View file

@ -7,7 +7,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Admin
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.Participant alias Mobilizon.Events.Participant
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
@ -321,64 +320,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end end
end end
def suspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: false} = actor <- Actors.get_remote_actor_with_preload(id),
{:ok, _} <- Actors.delete_actor(actor),
{:ok, _} <- Admin.log_action(moderator_actor, "suspend", actor) do
{:ok, actor}
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
%Actor{suspended: true} ->
{:error, "Actor already suspended"}
nil ->
{:error, "No remote profile found with this ID"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def suspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can suspend a profile"}
end
def unsuspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{preferred_username: preferred_username, domain: domain} = actor <-
Actors.get_remote_actor_with_preload(id, true),
{:ok, _} <- Actors.update_actor(actor, %{suspended: false}),
{:ok, %Actor{} = actor} <-
ActivityPub.make_actor_from_nickname("#{preferred_username}@#{domain}"),
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
{:ok, actor}
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
nil ->
{:error, "No remote profile found with this ID"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def unsuspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can unsuspend a profile"}
end
# We check that the actor is not the last administrator/creator of a group # We check that the actor is not the last administrator/creator of a group
@spec last_admin_of_a_group?(integer()) :: boolean() @spec last_admin_of_a_group?(integer()) :: boolean()
defp last_admin_of_a_group?(actor_id) do defp last_admin_of_a_group?(actor_id) do

View file

@ -3,6 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
Handles the posts-related GraphQL calls Handles the posts-related GraphQL calls
""" """
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Posts, Users} alias Mobilizon.{Actors, Posts, Users}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
@ -24,12 +25,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
%{page: page, limit: limit} = args, %{page: page, limit: limit} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_user: %User{role: user_role} = user
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do %Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
{:ok, page} {:ok, page}
else else

View file

@ -175,6 +175,7 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:discussion_mutations) import_fields(:discussion_mutations)
import_fields(:resource_mutations) import_fields(:resource_mutations)
import_fields(:post_mutations) import_fields(:post_mutations)
import_fields(:actor_mutations)
end end
@desc """ @desc """

View file

@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.GraphQL.Resolvers.Actor, as: ActorResolver
alias Mobilizon.GraphQL.Schema alias Mobilizon.GraphQL.Schema
import_types(Schema.Actors.FollowerType) import_types(Schema.Actors.FollowerType)
@ -59,4 +60,21 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
value(:Organization, description: "An ActivityPub Organization") value(:Organization, description: "An ActivityPub Organization")
value(:Service, description: "An ActivityPub Service") value(:Service, description: "An ActivityPub Service")
end end
object :actor_mutations do
field :suspend_profile, :deleted_object do
arg(:id, non_null(:id), description: "The profile ID to suspend")
resolve(&ActorResolver.suspend_profile/3)
end
field :unsuspend_profile, :actor do
arg(:id, non_null(:id), description: "The profile ID to unsuspend")
resolve(&ActorResolver.unsuspend_profile/3)
end
field :refresh_profile, :actor do
arg(:id, non_null(:id))
resolve(&ActorResolver.refresh_profile/3)
end
end
end end

View file

@ -130,11 +130,22 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
object :group_queries do object :group_queries do
@desc "Get all groups" @desc "Get all groups"
field :groups, :paginated_group_list do field :groups, :paginated_group_list do
arg(:preferred_username, :string, default_value: "")
arg(:name, :string, default_value: "")
arg(:domain, :string, default_value: "")
arg(:local, :boolean, default_value: true)
arg(:suspended, :boolean, default_value: false)
arg(:page, :integer, default_value: 1) arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10) arg(:limit, :integer, default_value: 10)
resolve(&Group.list_groups/3) resolve(&Group.list_groups/3)
end end
@desc "Get a group by its ID"
field :get_group, :group do
arg(:id, non_null(:id))
resolve(&Group.get_group/3)
end
@desc "Get a group by its preferred username" @desc "Get a group by its preferred username"
field :group, :group do field :group, :group do
arg(:preferred_username, non_null(:string)) arg(:preferred_username, non_null(:string))
@ -199,7 +210,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
@desc "Delete a group" @desc "Delete a group"
field :delete_group, :deleted_object do field :delete_group, :deleted_object do
arg(:group_id, non_null(:id)) arg(:group_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Group.delete_group/3) resolve(&Group.delete_group/3)
end end

View file

@ -188,16 +188,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
resolve(handle_errors(&Person.register_person/3)) resolve(handle_errors(&Person.register_person/3))
end end
field :suspend_profile, :deleted_object do
arg(:id, :id, description: "The profile ID to suspend")
resolve(&Person.suspend_profile/3)
end
field :unsuspend_profile, :person do
arg(:id, :id, description: "The profile ID to unsuspend")
resolve(&Person.unsuspend_profile/3)
end
end end
object :person_subscriptions do object :person_subscriptions do

View file

@ -33,6 +33,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
field(:inserted_at, :datetime) field(:inserted_at, :datetime)
field(:updated_at, :datetime) field(:updated_at, :datetime)
field(:deleted_at, :datetime) field(:deleted_at, :datetime)
field(:published_at, :datetime)
end end
@desc "The list of visibility options for a comment" @desc "The list of visibility options for a comment"

View file

@ -13,13 +13,12 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Actors.{Actor, Bot, Follower, Member} alias Mobilizon.Actors.{Actor, Bot, Follower, Member}
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.{Crypto, Events} alias Mobilizon.{Crypto, Events}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Service.Workers alias Mobilizon.Service.Workers
alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Web.Email.Group
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Web.Upload alias Mobilizon.Web.Upload
require Logger require Logger
@ -249,23 +248,19 @@ defmodule Mobilizon.Actors do
""" """
@spec upsert_actor(map, boolean) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} @spec upsert_actor(map, boolean) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def upsert_actor( def upsert_actor(
%{keys: keys, name: name, summary: summary, avatar: avatar, banner: banner} = data, data,
preload \\ false preload \\ false
) do ) do
# data =
# data
# |> Map.put(:avatar, transform_media_file(data.avatar))
# |> Map.put(:banner, transform_media_file(data.banner))
insert = insert =
data data
|> Actor.remote_actor_creation_changeset() |> Actor.remote_actor_creation_changeset()
|> Repo.insert( |> Repo.insert(
on_conflict: [ on_conflict: {:replace_all_except, [:id, :url, :preferred_username, :domain]},
set: [
keys: keys,
name: name,
summary: summary,
avatar: transform_media_file(avatar),
banner: transform_media_file(banner),
last_refreshed_at: DateTime.utc_now()
]
],
conflict_target: [:url] conflict_target: [:url]
) )
@ -282,26 +277,28 @@ defmodule Mobilizon.Actors do
end end
end end
defp transform_media_file(nil), do: nil # defp transform_media_file(nil), do: nil
defp transform_media_file(file) do # defp transform_media_file(file) do
file = for({key, val} <- file, into: %{}, do: {String.to_atom(key), val}) # file = for({key, val} <- file, into: %{}, do: {String.to_atom(key), val})
if is_nil(file) do # if is_nil(file) do
nil # nil
else # else
struct(Mobilizon.Media.File, file) # struct(Mobilizon.Media.File, file)
end # end
end # end
@delete_actor_default_options [reserve_username: true] @delete_actor_default_options [reserve_username: true, suspension: false]
def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
delete_actor_options = Keyword.merge(@delete_actor_default_options, options) delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
Workers.Background.enqueue("delete_actor", %{ Workers.Background.enqueue("delete_actor", %{
"actor_id" => actor.id, "actor_id" => actor.id,
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true) "author_id" => Keyword.get(delete_actor_options, :author_id),
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true),
"suspension" => Keyword.get(delete_actor_options, :suspension, false)
}) })
end end
@ -309,11 +306,16 @@ defmodule Mobilizon.Actors do
Deletes an actor. Deletes an actor.
""" """
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()} @spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do def perform(:delete_actor, %Actor{type: type} = actor, options \\ @delete_actor_default_options) do
Logger.info("Going to delete actor #{actor.url}") Logger.info("Going to delete actor #{actor.url}")
actor = Repo.preload(actor, @actor_preloads) actor = Repo.preload(actor, @actor_preloads)
delete_actor_options = Keyword.merge(@delete_actor_default_options, options) delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
Logger.debug(inspect(delete_actor_options))
if type == :Group do
delete_eventual_local_members(actor, delete_actor_options)
end
multi = multi =
Multi.new() Multi.new()
@ -322,6 +324,31 @@ defmodule Mobilizon.Actors do
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end) |> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end) |> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
multi =
if type == :Group do
multi
|> Multi.run(:delete_remote_members, fn _, _ ->
delete_group_elements(actor, :remote_members)
end)
|> Multi.run(:delete_group_organized_events, fn _, _ ->
delete_group_elements(actor, :events)
end)
|> Multi.run(:delete_group_posts, fn _, _ ->
delete_group_elements(actor, :posts)
end)
|> Multi.run(:delete_group_resources, fn _, _ ->
delete_group_elements(actor, :resources)
end)
|> Multi.run(:delete_group_todo_lists, fn _, _ ->
delete_group_elements(actor, :todo_lists)
end)
|> Multi.run(:delete_group_discussions, fn _, _ ->
delete_group_elements(actor, :discussions)
end)
else
multi
end
multi = multi =
if Keyword.get(delete_actor_options, :reserve_username, true) do if Keyword.get(delete_actor_options, :reserve_username, true) do
Multi.update(multi, :actor, Actor.delete_changeset(actor)) Multi.update(multi, :actor, Actor.delete_changeset(actor))
@ -329,6 +356,8 @@ defmodule Mobilizon.Actors do
Multi.delete(multi, :actor, actor) Multi.delete(multi, :actor, actor)
end end
Logger.debug("Going to run the transaction")
case Repo.transaction(multi) do case Repo.transaction(multi) do
{:ok, %{actor: %Actor{} = actor}} -> {:ok, %{actor: %Actor{} = actor}} ->
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}") {:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
@ -357,7 +386,16 @@ defmodule Mobilizon.Actors do
@doc """ @doc """
Returns the list of actors. Returns the list of actors.
""" """
@spec list_actors(String.t(), String.t(), boolean, boolean, integer, integer) :: Page.t() @spec list_actors(
atom(),
String.t(),
String.t(),
String.t(),
boolean,
boolean,
integer,
integer
) :: Page.t()
def list_actors( def list_actors(
type \\ :Person, type \\ :Person,
preferred_username \\ "", preferred_username \\ "",
@ -380,12 +418,41 @@ defmodule Mobilizon.Actors do
limit limit
) do ) do
person_query() person_query()
|> filter_actors(preferred_username, name, domain, local, suspended)
|> Page.build_page(page, limit)
end
def list_actors(
:Group,
preferred_username,
name,
domain,
local,
suspended,
page,
limit
) do
group_query()
|> filter_actors(preferred_username, name, domain, local, suspended)
|> Page.build_page(page, limit)
end
@spec filter_actors(Ecto.Query.t(), String.t(), String.t(), String.t(), boolean(), boolean()) ::
Ecto.Query.t()
defp filter_actors(
query,
preferred_username,
name,
domain,
local,
suspended
) do
query
|> filter_suspended(suspended) |> filter_suspended(suspended)
|> filter_preferred_username(preferred_username) |> filter_preferred_username(preferred_username)
|> filter_name(name) |> filter_name(name)
|> filter_domain(domain) |> filter_domain(domain)
|> filter_remote(local) |> filter_remote(local)
|> Page.build_page(page, limit)
end end
defp filter_preferred_username(query, ""), do: query defp filter_preferred_username(query, ""), do: query
@ -406,6 +473,7 @@ defmodule Mobilizon.Actors do
defp filter_remote(query, true), do: filter_local(query) defp filter_remote(query, true), do: filter_local(query)
defp filter_remote(query, false), do: filter_external(query) defp filter_remote(query, false), do: filter_external(query)
@spec filter_suspended(Ecto.Query.t(), boolean()) :: Ecto.Query.t()
defp filter_suspended(query, true), do: where(query, [a], a.suspended) defp filter_suspended(query, true), do: where(query, [a], a.suspended)
defp filter_suspended(query, false), do: where(query, [a], not a.suspended) defp filter_suspended(query, false), do: where(query, [a], not a.suspended)
@ -440,6 +508,7 @@ defmodule Mobilizon.Actors do
|> actor_by_username_or_name_query(term) |> actor_by_username_or_name_query(term)
|> actors_for_location(args) |> actors_for_location(args)
|> filter_by_types(types) |> filter_by_types(types)
|> filter_suspended(false)
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end
@ -492,6 +561,13 @@ defmodule Mobilizon.Actors do
|> Repo.one() |> Repo.one()
end end
@spec get_group_by_followers_url(String.t()) :: Actor.t()
def get_group_by_followers_url(followers_url) do
group_query()
|> where([q], q.followers_url == ^followers_url)
|> Repo.one()
end
@doc """ @doc """
Creates a group. Creates a group.
@ -702,6 +778,28 @@ defmodule Mobilizon.Actors do
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end
@spec list_local_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
def list_local_members_for_group(
%Actor{id: group_id, type: :Group} = _group,
page \\ nil,
limit \\ nil
) do
group_id
|> group_internal_member_query()
|> Page.build_page(page, limit)
end
@spec list_remote_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
def list_remote_members_for_group(
%Actor{id: group_id, type: :Group} = _group,
page \\ nil,
limit \\ nil
) do
group_id
|> group_external_member_query()
|> Page.build_page(page, limit)
end
@doc """ @doc """
Returns the list of members for a group. Returns the list of members for a group.
""" """
@ -1283,6 +1381,26 @@ defmodule Mobilizon.Actors do
|> select([_m, a], a) |> select([_m, a], a)
end end
@spec group_external_member_query(integer()) :: Ecto.Query.t()
defp group_external_member_query(group_id) do
Member
|> where([m], m.parent_id == ^group_id)
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|> where([_m, a], not is_nil(a.domain))
|> preload([m], [:parent, :actor])
|> select([m, _a], m)
end
@spec group_internal_member_query(integer()) :: Ecto.Query.t()
defp group_internal_member_query(group_id) do
Member
|> where([m], m.parent_id == ^group_id)
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|> where([_m, a], is_nil(a.domain))
|> preload([m], [:parent, :actor])
|> select([m, _a], m)
end
@spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t() @spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t()
def filter_member_role(query, []), do: query def filter_member_role(query, []), do: query
@ -1497,4 +1615,72 @@ defmodule Mobilizon.Actors do
{:error, res} {:error, res}
end end
end end
defp delete_group_elements(%Actor{type: :Group} = actor, type) do
Logger.debug("delete_group_elements #{inspect(type)}")
method =
case type do
:remote_members -> &list_remote_members_for_group/3
:events -> &Events.list_organized_events_for_group/3
:posts -> &Mobilizon.Posts.get_posts_for_group/3
:resources -> &Mobilizon.Resources.get_resources_for_group/3
:todo_lists -> &Mobilizon.Todos.get_todo_lists_for_group/3
:discussions -> &Mobilizon.Discussions.find_discussions_for_actor/3
end
res =
actor
|> accumulate_paginated_elements(method)
|> Enum.map(fn element -> ActivityPub.delete(element, actor, false) end)
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
Logger.debug("Return OK for all #{to_string(type)}")
{:ok, res}
else
Logger.debug("Something failed #{inspect(res)}")
{:error, res}
end
end
defp accumulate_paginated_elements(
%Actor{} = actor,
method,
elements \\ [],
page \\ 1,
limit \\ 10
) do
Logger.debug("accumulate_paginated_elements")
%Page{total: total, elements: new_elements} = page = method.(actor, page, limit)
elements = elements ++ new_elements
count = length(elements)
if count < total do
accumulate_paginated_elements(actor, method, elements, page + 1, limit)
else
Logger.debug("Found #{count} group elements to delete")
elements
end
end
# This one is not in the Multi transaction because it sends activities
defp delete_eventual_local_members(%Actor{} = group, options) do
suspended? = Keyword.get(options, :suspension, false)
group
|> accumulate_paginated_elements(&list_local_members_for_group/3)
|> Enum.map(fn member ->
if suspended? do
Group.send_group_suspension_notification(member)
else
with author_id when not is_nil(author_id) <- Keyword.get(options, :author_id),
%Actor{} = author <- get_actor(author_id) do
Group.send_group_deletion_notification(member, author)
end
end
member
end)
|> Enum.map(fn member -> ActivityPub.delete(member, group, false) end)
end
end end

View file

@ -94,6 +94,7 @@ defmodule Mobilizon.Discussions do
@spec get_comment!(integer | String.t()) :: Comment.t() @spec get_comment!(integer | String.t()) :: Comment.t()
def get_comment!(id), do: Repo.get!(Comment, id) def get_comment!(id), do: Repo.get!(Comment, id)
@spec get_comment_with_preload(String.t() | integer() | nil) :: Comment.t() | nil
def get_comment_with_preload(nil), do: nil def get_comment_with_preload(nil), do: nil
def get_comment_with_preload(id) do def get_comment_with_preload(id) do
@ -214,11 +215,20 @@ defmodule Mobilizon.Discussions do
But actually just empty the fields so that threads are not broken. But actually just empty the fields so that threads are not broken.
""" """
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()} @spec delete_comment(Comment.t(), Keyword.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def delete_comment(%Comment{} = comment) do def delete_comment(%Comment{} = comment, options \\ []) do
comment if Keyword.get(options, :force, false) == false do
|> Comment.delete_changeset() with {:ok, %Comment{} = comment} <-
|> Repo.update() comment
|> Comment.delete_changeset()
|> Repo.update(),
%Comment{} = comment <- get_comment_with_preload(comment.id) do
{:ok, comment}
end
else
comment
|> Repo.delete()
end
end end
@doc """ @doc """
@ -289,8 +299,8 @@ defmodule Mobilizon.Discussions do
|> Repo.preload(@discussion_preloads) |> Repo.preload(@discussion_preloads)
end end
@spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t() @spec find_discussions_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do def find_discussions_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
Discussion Discussion
|> where([c], c.actor_id == ^actor_id) |> where([c], c.actor_id == ^actor_id)
|> preload(^@discussion_preloads) |> preload(^@discussion_preloads)
@ -372,9 +382,13 @@ defmodule Mobilizon.Discussions do
Delete a discussion. Delete a discussion.
""" """
@spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()} @spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
def delete_discussion(%Discussion{} = discussion) do def delete_discussion(%Discussion{id: discussion_id}) do
discussion Multi.new()
|> Repo.delete() |> Multi.delete_all(:comments, fn _ ->
where(Comment, [c], c.discussion_id == ^discussion_id)
end)
# |> Multi.delete(:discussion, discussion)
|> Repo.transaction()
end end
defp public_comments_for_actor_query(actor_id) do defp public_comments_for_actor_query(actor_id) do
@ -402,7 +416,4 @@ defmodule Mobilizon.Discussions do
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t() @spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
defp preload_for_comment(query), do: preload(query, ^@comment_preloads) defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
# @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t()
# defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads)
end end

View file

@ -46,13 +46,6 @@ defmodule Mobilizon.Posts do
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end
def do_get_posts_for_group(group_id) do
Post
|> where(attributed_to_id: ^group_id)
|> order_by(desc: :inserted_at)
|> preload([p], [:author, :attributed_to, :picture])
end
@doc """ @doc """
Get a post by it's ID Get a post by it's ID
""" """
@ -144,4 +137,11 @@ defmodule Mobilizon.Posts do
defp filter_public(query) do defp filter_public(query) do
where(query, [p], p.visibility == ^:public and not p.draft) where(query, [p], p.visibility == ^:public and not p.draft)
end end
defp do_get_posts_for_group(group_id) do
Post
|> where(attributed_to_id: ^group_id)
|> order_by(desc: :inserted_at)
|> preload([p], [:author, :attributed_to, :picture])
end
end end

View file

@ -7,6 +7,7 @@ defmodule Mobilizon.Tombstone do
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
require Ecto.Query
@type t :: %__MODULE__{ @type t :: %__MODULE__{
uri: String.t(), uri: String.t(),
@ -42,4 +43,17 @@ defmodule Mobilizon.Tombstone do
def find_tombstone(uri) do def find_tombstone(uri) do
Repo.get_by(__MODULE__, uri: uri) Repo.get_by(__MODULE__, uri: uri)
end end
@spec delete_actor_tombstones(String.t() | integer()) :: {integer(), nil}
def delete_actor_tombstones(actorId) do
__MODULE__
|> Ecto.Query.where(actor_id: ^actorId)
|> Repo.delete_all()
end
def delete_uri_tombstone(uri) do
__MODULE__
|> Ecto.Query.where(uri: ^uri)
|> Repo.delete_all()
end
end end

View file

@ -7,7 +7,7 @@ defmodule Mobilizon.Web.Email.Group do
import Bamboo.Phoenix import Bamboo.Phoenix
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
alias Mobilizon.{Actors, Users} alias Mobilizon.{Actors, Config, Users}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.{Email, Gettext}
@ -68,7 +68,7 @@ defmodule Mobilizon.Web.Email.Group do
|> assign(:locale, locale) |> assign(:locale, locale)
|> assign(:group, group) |> assign(:group, group)
|> assign(:subject, subject) |> assign(:subject, subject)
|> render(:group_removal) |> render(:group_member_removal)
|> Email.Mailer.deliver_later() |> Email.Mailer.deliver_later()
:ok :ok
@ -76,4 +76,83 @@ defmodule Mobilizon.Web.Email.Group do
end end
# TODO : def send_confirmation_to_inviter() # TODO : def send_confirmation_to_inviter()
@member_roles [:administrator, :moderator, :member]
def send_group_suspension_notification(%Member{actor: %Actor{user_id: nil}}), do: :ok
def send_group_suspension_notification(%Member{role: role}) when role not in @member_roles,
do: :ok
def send_group_suspension_notification(%Member{
actor: %Actor{user_id: user_id},
parent: %Actor{domain: nil} = group,
role: member_role
}) do
with %User{email: email, locale: locale} <- Users.get_user!(user_id) do
Gettext.put_locale(locale)
instance = Config.instance_name()
subject =
gettext(
"The group %{group} has been suspended on %{instance}",
group: group.name,
instance: instance
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:group, group)
|> assign(:role, member_role)
|> assign(:subject, subject)
|> assign(:instance, instance)
|> render(:group_suspension)
|> Email.Mailer.deliver_later()
:ok
end
end
def send_group_deletion_notification(%Member{actor: %Actor{user_id: nil}}, _author), do: :ok
def send_group_deletion_notification(%Member{role: role}, _author)
when role not in @member_roles,
do: :ok
def send_group_deletion_notification(
%Member{
actor: %Actor{user_id: user_id, id: actor_id},
parent: %Actor{domain: nil} = group,
role: member_role
},
%Actor{id: author_id} = author
) do
with %User{email: email, locale: locale} <- Users.get_user!(user_id),
{:member_not_author, true} <- {:member_not_author, author_id !== actor_id} do
Gettext.put_locale(locale)
instance = Config.instance_name()
subject =
gettext(
"The group %{group} has been deleted on %{instance}",
group: group.name,
instance: instance
)
Email.base_email(to: email, subject: subject)
|> assign(:locale, locale)
|> assign(:group, group)
|> assign(:role, member_role)
|> assign(:subject, subject)
|> assign(:instance, instance)
|> assign(:author, author)
|> render(:group_deletion)
|> Email.Mailer.deliver_later()
:ok
else
# Skip if it's the author itself
{:member_not_author, _} ->
:ok
end
end
end end

View file

@ -0,0 +1,51 @@
<!-- HERO -->
<tr>
<td bgcolor="#474467" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #3A384C; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "The group %{group} was deleted on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#E6E4F4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted.",
author: Mobilizon.Actors.Actor.display_name_and_username(@author),
group: Mobilizon.Actors.Actor.display_name_and_username(@group) %>
</p>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View file

@ -0,0 +1,5 @@
<%= gettext "The group %{group} was deleted on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
==
<%= gettext "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted.",
author: Mobilizon.Actors.Actor.display_name_and_username(@author),
group: Mobilizon.Actors.Actor.display_name_and_username(@group) %>

View file

@ -0,0 +1,66 @@
<!-- HERO -->
<tr>
<td bgcolor="#474467" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #3A384C; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
<%= gettext "The group %{group} has been suspended on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
</h1>
</td>
</tr>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- COPY BLOCK -->
<tr>
<td bgcolor="#E6E4F4" align="center" style="padding: 0px 10px 0px 10px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
<!-- COPY -->
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;">
<%= gettext "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group.", group_name: @group.name, group_address: if @group.domain, do: "@#{@group.preferred_username}@#{@group.domain}", else: "@#{@group.preferred_username}" %>
</p>
</td>
</tr>
<%= if is_nil(@group.domain) do %>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "As this group was located on this instance, all of it's data has been irretrievably deleted." %>
</p>
</td>
</tr>
<% else %>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #474467; font-family: 'Roboto', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0">
<%= gettext "As this group was located on another instance, it will continue to work for other instances than this one." %>
</p>
</td>
</tr>
<% end %>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>

View file

@ -0,0 +1,8 @@
<%= gettext "The group %{group} has been suspended on %{instance}!", group: (@group.name || @group.preferred_username), instance: @instance %>
==
<%= gettext "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group.", group_name: @group.name, group_address: if @group.domain, do: "@#{@group.preferred_username}@#{@group.domain}", else: "@#{@group.preferred_username}" %>
<%= if is_nil(@group.domain) do %>
<%= gettext "As this group was located on this instance, all of it's data has been irretrievably deleted." %>
<% else %>
<%= gettext "As this group was located on another instance, it will continue to work for other instances than this one." %>
<% end %>

View file

@ -88,7 +88,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do
end end
defp fetch_collection(:discussions, actor, page) do defp fetch_collection(:discussions, actor, page) do
Discussions.find_discussions_for_actor(actor.id, page) Discussions.find_discussions_for_actor(actor, page)
end end
defp fetch_collection(:posts, actor, page) do defp fetch_collection(:posts, actor, page) do

View file

@ -407,7 +407,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1209,33 +1209,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -398,7 +398,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1185,33 +1185,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -406,7 +406,7 @@ msgid "View the event on: %{link}"
msgstr "Vés a l'activitat actualitzada a %{link}" msgstr "Vés a l'activitat actualitzada a %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1205,33 +1205,79 @@ msgstr ""
"meitat del 2020%{b_end}." "meitat del 2020%{b_end}."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -398,7 +398,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1185,33 +1185,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -411,7 +411,7 @@ msgid "View the event on: %{link}"
msgstr "Zeige die aktualisierte Veranstaltung unter: %{link}" msgstr "Zeige die aktualisierte Veranstaltung unter: %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1213,33 +1213,79 @@ msgstr ""
"in der ersten Hälfte von 2020 veröffentlicht wird</b>." "in der ersten Hälfte von 2020 veröffentlicht wird</b>."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -382,7 +382,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1164,24 +1164,24 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
@ -1191,6 +1191,52 @@ msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -405,7 +405,7 @@ msgid "View the event on: %{link}"
msgstr "View the updated event on: %{link}" msgstr "View the updated event on: %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1187,33 +1187,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020." msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,16 +10,15 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: 2020-08-18 12:37+0000\n" "PO-Revision-Date: 2020-08-27 12:12+0200\n"
"Last-Translator: Thomas Citharel <thomas.citharel@framasoft.org>\n" "Last-Translator: Thomas Citharel <thomas.citharel@framasoft.org>\n"
"Language-Team: French <https://weblate.framasoft.org/projects/mobilizon/" "Language-Team: French <https://weblate.framasoft.org/projects/mobilizon/backend/fr/>\n"
"backend/fr/>\n"
"Language: fr\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n" "Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.1\n" "X-Generator: Poedit 2.3\n"
#: lib/web/templates/email/password_reset.html.eex:48 #: lib/web/templates/email/password_reset.html.eex:48
msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one."
@ -334,7 +333,7 @@ msgstr "Pour accepter cette invitation, rendez-vous dans vos groupes."
msgid "View the event on: %{link}" msgid "View the event on: %{link}"
msgstr "Voir l'événement mis à jour sur : %{link}" msgstr "Voir l'événement mis à jour sur : %{link}"
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "Vous avez été invité par %{inviter} à rejoindre le groupe %{group}" msgstr "Vous avez été invité par %{inviter} à rejoindre le groupe %{group}"
@ -817,12 +816,8 @@ msgid "In the meantime, please consider this software as not (yet) fully functio
msgstr "D'ici là, veuillez considérer que le logiciel n'est pas (encore) fini. Retrouvez plus d'informations %{a_start}sur le blog de Framasoft%{a_end}." msgstr "D'ici là, veuillez considérer que le logiciel n'est pas (encore) fini. Retrouvez plus d'informations %{a_start}sur le blog de Framasoft%{a_end}."
#: lib/web/templates/email/email.text.eex:10 #: lib/web/templates/email/email.text.eex:10
msgid "" msgid "In the meantime, please consider this software as not (yet) fully functional. Read more on the Framasoft blog:"
"In the meantime, please consider this software as not (yet) fully " msgstr "D'ici là, veuillez considérer que le logiciel n'est pas (encore) fini. Retrouvez plus d'informations sur le blog de Framasoft:"
"functional. Read more on the Framasoft blog:"
msgstr ""
"D'ici là, veuillez considérer que le logiciel n'est pas (encore) fini. "
"Retrouvez plus d'informations sur le blog de Framasoft:"
#: lib/web/templates/email/email.html.eex:147 lib/web/templates/email/email.text.eex:16 #: lib/web/templates/email/email.html.eex:147 lib/web/templates/email/email.text.eex:16
msgid "Learn more about Mobilizon here!" msgid "Learn more about Mobilizon here!"
@ -952,19 +947,19 @@ msgstr "<b>Veuillez ne pas l'utiliser pour un cas réel.</b>"
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>." msgid "Mobilizon is still under development, we will add new features along the updates, until the release of <b>version 1 of the software in the fall of 2020</b>."
msgstr "Mobilizon est en cours de développement, nous y ajouterons de nouvelles fonctionnalités lors de mises à jour régulières, jusqu'à la publication de <b>la version 1 du logiciel à l'automne 2020</b>." msgstr "Mobilizon est en cours de développement, nous y ajouterons de nouvelles fonctionnalités lors de mises à jour régulières, jusqu'à la publication de <b>la version 1 du logiciel à l'automne 2020</b>."
#: lib/web/templates/email/group_removal.html.eex:45 lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.html.eex:45 lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "Si vous pensez qu'il s'agit d'une erreur, vous pouvez contacter les administrateurs du groupe afin qu'ils vous réintègrent." msgstr "Si vous pensez qu'il s'agit d'une erreur, vous pouvez contacter les administrateurs du groupe afin qu'ils vous réintègrent."
#: lib/web/templates/email/group_removal.html.eex:13 lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.html.eex:13 lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "Salut, et encore merci pour le poisson !" msgstr "Salut, et encore merci pour le poisson !"
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "Vous avez été enlevé du groupe %{group}" msgstr "Vous avez été enlevé du groupe %{group}"
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "Vous avez été enlevé du groupe %{group}. Vous ne serez plus en mesure d'accéder au contenu privé du groupe." msgstr "Vous avez été enlevé du groupe %{group}. Vous ne serez plus en mesure d'accéder au contenu privé du groupe."
@ -972,6 +967,38 @@ msgstr "Vous avez été enlevé du groupe %{group}. Vous ne serez plus en mesure
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "<b>%{inviter}</b> vient de vous inviter à rejoindre son groupe %{link_start}<b>%{group}</b>%{link_end}" msgstr "<b>%{inviter}</b> vient de vous inviter à rejoindre son groupe %{link_start}<b>%{group}</b>%{link_end}"
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "Vous avez été enlevé du groupe %{link_start}<b>%{group}</b>%{link_end}. Vous ne serez plus en mesure d'accéder au contenu privé du groupe." msgstr "Vous avez été enlevé du groupe %{link_start}<b>%{group}</b>%{link_end}. Vous ne serez plus en mesure d'accéder au contenu privé du groupe."
#: lib/web/templates/email/group_suspension.html.eex:54 lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr "Comme ce groupe était originaire d'une autre instance, il continuera à fonctionner pour d'autres instances que celle-ci."
#: lib/web/templates/email/group_suspension.html.eex:46 lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr "Comme ce groupe était originaire de cette instance, toutes ses données ont été irrémédiablement détruites."
#: lib/web/templates/email/group_deletion.html.eex:38 lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr "L'administrateur·ice %{author} a supprimé le groupe %{group}. Tous les événements, discussions, billets et todos du groupe ont été supprimés."
#: lib/web/templates/email/group_suspension.html.eex:13 lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr "Le groupe %{group} a été suspendu sur %{instance} !"
#: lib/web/templates/email/group_deletion.html.eex:13 lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr "Le groupe %{group} a été supprimé sur %{instance} !"
#: lib/web/templates/email/group_suspension.html.eex:38 lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr "L'équipe de modération de votre instance a décidé de suspendre %{group_name} (%{group_address}). Vous n'êtes désormais plus membre de ce groupe."
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr "Le groupe %{group} a été supprimé sur %{instance}"
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr "Le groupe %{group} a été suspendu sur %{instance}"

View file

@ -398,7 +398,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1181,33 +1181,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -394,7 +394,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1171,33 +1171,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -407,7 +407,7 @@ msgid "View the event on: %{link}"
msgstr "Bekijk het bijgewerkte evenement op: %{link}" msgstr "Bekijk het bijgewerkte evenement op: %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1208,33 +1208,79 @@ msgstr ""
"beschikbaar is in de eerste helft van 2020</b>." "beschikbaar is in de eerste helft van 2020</b>."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -405,7 +405,7 @@ msgid "View the event on: %{link}"
msgstr "Veire leveniment actualizat sus : %{link}" msgstr "Veire leveniment actualizat sus : %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "%{inviter} vos a convidat a rejónher lo grop %{group}" msgstr "%{inviter} vos a convidat a rejónher lo grop %{group}"
@ -1210,33 +1210,79 @@ msgstr ""
"la version 1 del logicial al primièr semèstre 2020</b>." "la version 1 del logicial al primièr semèstre 2020</b>."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -413,7 +413,7 @@ msgid "View the event on: %{link}"
msgstr "Zobacz zaktualizowane wydarzenie na %{link}" msgstr "Zobacz zaktualizowane wydarzenie na %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1221,33 +1221,79 @@ msgstr ""
"roku</b>." "roku</b>."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -396,7 +396,7 @@ msgid "View the event on: %{link}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1178,33 +1178,79 @@ msgid "Mobilizon is still under development, we will add new features along the
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

View file

@ -444,7 +444,7 @@ msgid "View the event on: %{link}"
msgstr "Veja o evento atualizado em: %{link}" msgstr "Veja o evento atualizado em: %{link}"
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:28 #: lib/web/email/group.ex:32
msgid "You have been invited by %{inviter} to join group %{group}" msgid "You have been invited by %{inviter} to join group %{group}"
msgstr "" msgstr ""
@ -1300,33 +1300,79 @@ msgstr ""
"1 do aplicativo no primeiro semestre de 2020</b>." "1 do aplicativo no primeiro semestre de 2020</b>."
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:45 #: lib/web/templates/email/group_member_removal.html.eex:45
#: lib/web/templates/email/group_removal.text.eex:5 #: lib/web/templates/email/group_member_removal.text.eex:5
msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back." msgid "If you feel this is an error, you may contact the group's administrators so that they can add you back."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:13 #: lib/web/templates/email/group_member_removal.html.eex:13
#: lib/web/templates/email/group_removal.text.eex:1 #: lib/web/templates/email/group_member_removal.text.eex:1
msgid "So long, and thanks for the fish!" msgid "So long, and thanks for the fish!"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/email/group.ex:58 #: lib/web/email/group.ex:62
msgid "You have been removed from group %{group}" msgid "You have been removed from group %{group}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/web/templates/email/group_removal.text.eex:3 #: lib/web/templates/email/group_member_removal.text.eex:3
msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{group}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_invite.html.eex:38 #: lib/web/templates/email/group_invite.html.eex:38
msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}" msgid "<b>%{inviter}</b> just invited you to join their group %{link_start}<b>%{group}</b>%{link_end}"
msgstr "" msgstr ""
#, elixir-format, fuzzy #, elixir-format
#: lib/web/templates/email/group_removal.html.eex:38 #: lib/web/templates/email/group_member_removal.html.eex:38
msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore." msgid "You have been removed from group %{link_start}<b>%{group}</b>%{link_end}. You will not be able to access this group's private content anymore."
msgstr "" msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:54
#: lib/web/templates/email/group_suspension.text.eex:7
msgid "As this group was located on another instance, it will continue to work for other instances than this one."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:46
#: lib/web/templates/email/group_suspension.text.eex:5
msgid "As this group was located on this instance, all of it's data has been irretrievably deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:38
#: lib/web/templates/email/group_deletion.text.eex:3
msgid "The administrator %{author} deleted group %{group}. All of the group's events, discussions, posts and todos have been deleted."
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:13
#: lib/web/templates/email/group_suspension.text.eex:1
msgid "The group %{group} has been suspended on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_deletion.html.eex:13
#: lib/web/templates/email/group_deletion.text.eex:1
msgid "The group %{group} was deleted on %{instance}!"
msgstr ""
#, elixir-format
#: lib/web/templates/email/group_suspension.html.eex:38
#: lib/web/templates/email/group_suspension.text.eex:3
msgid "Your instance's moderation team has decided to suspend %{group_name} (%{group_address}). You are no longer a member of this group."
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:135
msgid "The group %{group} has been deleted on %{instance}"
msgstr ""
#, elixir-format, fuzzy
#: lib/web/email/group.ex:96
msgid "The group %{group} has been suspended on %{instance}"
msgstr ""

Some files were not shown because too many files have changed in this diff Show more