Merge branch 'analytics' into 'main'

Provide analytics on Front-end

Closes #690

See merge request framasoft/mobilizon!1200
This commit is contained in:
Thomas Citharel 2022-04-06 18:31:49 +00:00
commit ca6db74a73
43 changed files with 1017 additions and 390 deletions

View File

@ -110,7 +110,7 @@ deps:
exunit: exunit:
stage: test stage: test
services: services:
- name: postgis/postgis:13-3.1 - name: postgis/postgis:14-3.2
alias: postgres alias: postgres
variables: variables:
MIX_ENV: test MIX_ENV: test

View File

@ -345,6 +345,8 @@ config :mobilizon, :exports,
Mobilizon.Service.Export.Participants.CSV Mobilizon.Service.Export.Participants.CSV
] ]
config :mobilizon, :analytics, providers: []
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@ -1,15 +1,11 @@
FROM elixir:latest FROM elixir:latest
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>" LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
ENV REFRESHED_AT=2021-12-15 ENV REFRESHED_AT=2022-04-06
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
RUN npm install -g yarn wait-on RUN npm install -g yarn wait-on
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mix local.hex --force && mix local.rebar --force RUN mix local.hex --force && mix local.rebar --force
# Weasyprint 53 requires pango >= 1.44.0, which is not available in Stretch. RUN pip3 install -Iv weasyprint pyexcel_ods3
# TODO: Remove the version requirement when elixir:latest is based on Bullseye
# https://github.com/erlang/docker-erlang-otp/issues/362
# https://github.com/Kozea/WeasyPrint/issues/1384
RUN pip3 install -Iv weasyprint==52 pyexcel_ods3
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/ RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/

View File

@ -8,7 +8,7 @@
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit", "test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"build:assets": "vue-cli-service build", "build:assets": "vue-cli-service build --report",
"build:pictures": "bash ./scripts/build/pictures.sh" "build:pictures": "bash ./scripts/build/pictures.sh"
}, },
"dependencies": { "dependencies": {
@ -16,6 +16,8 @@
"@absinthe/socket-apollo-link": "^0.2.1", "@absinthe/socket-apollo-link": "^0.2.1",
"@apollo/client": "^3.3.16", "@apollo/client": "^3.3.16",
"@mdi/font": "^6.1.95", "@mdi/font": "^6.1.95",
"@sentry/tracing": "^6.16.1",
"@sentry/vue": "^6.16.1",
"@tailwindcss/line-clamp": "^0.3.0", "@tailwindcss/line-clamp": "^0.3.0",
"@tiptap/core": "^2.0.0-beta.41", "@tiptap/core": "^2.0.0-beta.41",
"@tiptap/extension-blockquote": "^2.0.0-beta.25", "@tiptap/extension-blockquote": "^2.0.0-beta.25",
@ -69,7 +71,9 @@
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-class-component": "^7.2.3", "vue-class-component": "^7.2.3",
"vue-i18n": "^8.14.0", "vue-i18n": "^8.14.0",
"vue-matomo": "^4.1.0",
"vue-meta": "^2.3.1", "vue-meta": "^2.3.1",
"vue-plausible": "^1.3.1",
"vue-property-decorator": "^9.0.0", "vue-property-decorator": "^9.0.0",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vue-scrollto": "^2.17.1", "vue-scrollto": "^2.17.1",

View File

@ -203,6 +203,16 @@ export default class App extends Vue {
this.interval = undefined; this.interval = undefined;
} }
@Watch("config")
async initializeStatistics(config: IConfig) {
if (config) {
const { statistics } = (await import("./services/statistics")) as {
statistics: (config: IConfig, environment: Record<string, any>) => void;
};
statistics(config, { router: this.$router, version: config.version });
}
}
@Watch("$route", { immediate: true }) @Watch("$route", { immediate: true })
updateAnnouncement(route: Route): void { updateAnnouncement(route: Route): void {
const pageTitle = this.extractPageTitleFromRoute(route); const pageTitle = this.extractPageTitleFromRoute(route);

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="w-80 bg-white rounded-lg shadow-md flex space-x-4 items-center" class="bg-white rounded-lg flex space-x-4 items-center"
:class="{ 'flex-col p-4 sm:p-8 pb-10': !inline }" :class="{ 'flex-col p-4 shadow-md sm:p-8 pb-10 w-80': !inline }"
> >
<div> <div>
<figure class="w-12 h-12" v-if="actor.avatar"> <figure class="w-12 h-12" v-if="actor.avatar">
@ -20,8 +20,10 @@
class="ltr:-mr-0.5 rtl:-ml-0.5" class="ltr:-mr-0.5 rtl:-ml-0.5"
/> />
</div> </div>
<div :class="{ 'text-center': !inline }"> <div :class="{ 'text-center': !inline }" class="overflow-hidden w-full">
<h5 class="text-xl font-medium violet-title tracking-tight text-gray-900"> <h5
class="text-xl font-medium violet-title tracking-tight text-gray-900 whitespace-pre-line line-clamp-2"
>
{{ displayName(actor) }} {{ displayName(actor) }}
</h5> </h5>
<p class="text-gray-500 truncate" v-if="actor.name"> <p class="text-gray-500 truncate" v-if="actor.name">

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="ellipsis" class="truncate"
:title=" :title="
isDescriptionDifferentFromLocality isDescriptionDifferentFromLocality
? `${physicalAddress.description}, ${physicalAddress.locality}` ? `${physicalAddress.description}, ${physicalAddress.locality}`
@ -8,8 +8,7 @@
" "
> >
<b-icon icon="map-marker" /> <b-icon icon="map-marker" />
<span v-if="isDescriptionDifferentFromLocality"> <span v-if="physicalAddress.locality">
{{ physicalAddress.description }},
{{ physicalAddress.locality }} {{ physicalAddress.locality }}
</span> </span>
<span v-else> <span v-else>
@ -35,11 +34,3 @@ export default class InlineAddress extends Vue {
} }
} }
</script> </script>
<style lang="scss" scoped>
.ellipsis {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -48,13 +48,75 @@
$t("Mobilizon") $t("Mobilizon")
}}</a> }}</a>
</i18n> </i18n>
{{ <span v-if="sentryEnabled && sentryReady">
$t( {{
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):" $t(
) "We collect your feedback and the error information in order to improve this service."
}} )
}}</span
>
<span v-else>
{{
$t(
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
)
}}
</span>
</p> </p>
<div class="content"> <form
v-if="sentryEnabled && sentryReady && !submittedFeedback"
@submit.prevent="sendErrorToSentry"
>
<b-field :label="$t('What happened?')" label-for="what-happened">
<b-input
v-model="feedback"
type="textarea"
id="what-happened"
:placeholder="$t(`I've clicked on X, then on Y`)"
/>
</b-field>
<b-button icon-left="send" native-type="submit" type="is-primary">{{
$t("Send feedback")
}}</b-button>
<p class="content">
{{
$t(
"Please add as many details as possible to help identify the problem."
)
}}
</p>
</form>
<b-message type="is-danger" v-else-if="feedbackError">
<p>
{{
$t(
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway."
)
}}
</p>
<i18n path="You may now close this page or {return_to_the_homepage}.">
<template #return_to_the_homepage>
<router-link :to="{ name: RouteName.HOME }">{{
$t("return to the homepage")
}}</router-link>
</template>
</i18n>
</b-message>
<b-message type="is-success" v-else-if="submittedFeedback">
<p>{{ $t("Thanks a lot, your feedback was submitted!") }}</p>
<i18n path="You may now close this page or {return_to_the_homepage}.">
<template #return_to_the_homepage>
<router-link :to="{ name: RouteName.HOME }">{{
$t("return to the homepage")
}}</router-link>
</template>
</i18n>
</b-message>
<div
class="content"
v-if="!(sentryEnabled && sentryReady) || submittedFeedback"
>
<p v-if="submittedFeedback">{{ $t("You may also:") }}</p>
<ul> <ul>
<li> <li>
<a <a
@ -65,7 +127,7 @@
</li> </li>
<li> <li>
<a <a
href="https://framagit.org/framasoft/mobilizon/-/issues/new?issuable_template=Bug" href="https://framagit.org/framasoft/mobilizon/-/issues/"
target="_blank" target="_blank"
>{{ >{{
$t("Open an issue on our bug tracker (advanced users)") $t("Open an issue on our bug tracker (advanced users)")
@ -74,7 +136,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<p class="content"> <p class="content" v-if="!sentryEnabled">
{{ {{
$t( $t(
"Please add as many details as possible to help identify the problem." "Please add as many details as possible to help identify the problem."
@ -89,14 +151,14 @@
<p>{{ $t("Error stacktrace") }}</p> <p>{{ $t("Error stacktrace") }}</p>
<pre>{{ error.stack }}</pre> <pre>{{ error.stack }}</pre>
</details> </details>
<p> <p v-if="!sentryEnabled">
{{ {{
$t( $t(
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback." "The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
) )
}} }}
</p> </p>
<div class="buttons"> <div class="buttons" v-if="!sentryEnabled">
<b-tooltip <b-tooltip
:label="tooltipConfig.label" :label="tooltipConfig.label"
:type="tooltipConfig.type" :type="tooltipConfig.type"
@ -115,14 +177,20 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { CONTACT } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import { checkProviderConfig, convertConfig } from "@/services/statistics";
import { IAnalyticsConfig, IConfig } from "@/types/config.model";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { LOGGED_USER } from "@/graphql/user";
import { IUser } from "@/types/current-user.model";
import { ISentryConfiguration } from "@/types/analytics/sentry.model";
import { submitFeedback } from "@/services/statistics/sentry";
import RouteName from "@/router/name";
@Component({ @Component({
apollo: { apollo: {
config: { config: CONFIG,
query: CONTACT, loggedUser: LOGGED_USER,
},
}, },
metaInfo() { metaInfo() {
return { return {
@ -138,7 +206,17 @@ export default class ErrorComponent extends Vue {
copied: "success" | "error" | false = false; copied: "success" | "error" | false = false;
config!: { contact: string | null; name: string }; config!: IConfig;
feedback = "";
submittedFeedback = false;
feedbackError = false;
loggedUser!: IUser;
RouteName = RouteName;
async copyErrorToClipboard(): Promise<void> { async copyErrorToClipboard(): Promise<void> {
try { try {
@ -193,6 +271,56 @@ export default class ErrorComponent extends Vue {
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
get sentryEnabled(): boolean {
return this.sentryProvider?.enabled === true;
}
get sentryProvider(): IAnalyticsConfig | undefined {
return this.config && checkProviderConfig(this.config, "sentry");
}
get sentryConfig(): ISentryConfiguration | undefined {
if (this.sentryProvider?.configuration) {
return convertConfig(
this.sentryProvider?.configuration
) as ISentryConfiguration;
}
return undefined;
}
get sentryReady() {
const eventId = window.sessionStorage.getItem("lastEventId");
const dsn = this.sentryConfig?.dsn;
const organization = this.sentryConfig?.organization;
const project = this.sentryConfig?.project;
const host = this.sentryConfig?.host;
return eventId && dsn && organization && project && host;
}
async sendErrorToSentry() {
try {
const eventId = window.sessionStorage.getItem("lastEventId");
const dsn = this.sentryConfig?.dsn;
const organization = this.sentryConfig?.organization;
const project = this.sentryConfig?.project;
const host = this.sentryConfig?.host;
const endpoint = `https://${host}/api/0/projects/${organization}/${project}/user-feedback/`;
if (eventId && dsn && this.sentryReady) {
await submitFeedback(endpoint, dsn, {
event_id: eventId,
name:
this.loggedUser?.defaultActor?.preferredUsername || "Unknown user",
email: this.loggedUser?.email || "unknown@email.org",
comments: this.feedback,
});
this.submittedFeedback = true;
}
} catch (error) {
console.error(error);
this.feedbackError = true;
}
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -67,7 +67,6 @@
<inline-address <inline-address
dir="auto" dir="auto"
v-if="event.physicalAddress" v-if="event.physicalAddress"
class="event-subtitle"
:physical-address="event.physicalAddress" :physical-address="event.physicalAddress"
/> />
<div <div

View File

@ -36,6 +36,7 @@
> >
<router-link <router-link
v-if="event.attributedTo" v-if="event.attributedTo"
class="hover:underline"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { params: {
@ -53,6 +54,7 @@
</router-link> </router-link>
<actor-card v-else :actor="event.organizerActor" :inline="true" /> <actor-card v-else :actor="event.organizerActor" :inline="true" />
<actor-card <actor-card
:inline="true"
:actor="contact" :actor="contact"
v-for="contact in event.contacts" v-for="contact in event.contacts"
:key="contact.id" :key="contact.id"
@ -65,6 +67,7 @@
> >
<a <a
target="_blank" target="_blank"
class="hover:underline"
rel="noopener noreferrer ugc" rel="noopener noreferrer ugc"
:href="event.onlineAddress" :href="event.onlineAddress"
:title=" :title="

View File

@ -1,6 +1,6 @@
<template> <template>
<router-link <router-link
class="event-minimalist-card-wrapper" class="event-minimalist-card-wrapper bg-white rounded-lg shadow-md"
dir="auto" dir="auto"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }" :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
> >

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="empty-content" :class="{ inline }" role="note"> <div
class="empty-content"
:class="{ inline, 'text-center': center }"
role="note"
>
<b-icon :icon="icon" size="is-large" /> <b-icon :icon="icon" size="is-large" />
<h2 class="empty-content__title"> <h2 class="empty-content__title">
<!-- @slot Mandatory title --> <!-- @slot Mandatory title -->
@ -18,6 +22,7 @@ import { Component, Prop, Vue } from "vue-property-decorator";
export default class EmptyContent extends Vue { export default class EmptyContent extends Vue {
@Prop({ type: String, required: true }) icon!: string; @Prop({ type: String, required: true }) icon!: string;
@Prop({ type: Boolean, required: false, default: false }) inline!: boolean; @Prop({ type: Boolean, required: false, default: false }) inline!: boolean;
@Prop({ type: Boolean, required: false, default: false }) center!: boolean;
} }
</script> </script>

View File

@ -96,6 +96,15 @@ export const CONFIG = gql`
enabled enabled
publicKey publicKey
} }
analytics {
id
enabled
configuration {
key
value
type
}
}
} }
} }
`; `;

View File

@ -1309,5 +1309,17 @@
"Reset filters": "Reset filters", "Reset filters": "Reset filters",
"Category": "Category", "Category": "Category",
"Select a category": "Select a category", "Select a category": "Select a category",
"Any category": "Any category" "Any category": "Any category",
} "We collect your feedback and the error information in order to improve this service.": "We collect your feedback and the error information in order to improve this service.",
"What happened?": "What happened?",
"I've clicked on X, then on Y": "I've clicked on X, then on Y",
"Send feedback": "Send feedback",
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.",
"return to the homepage": "return to the homepage",
"Thanks a lot, your feedback was submitted!": "Thanks a lot, your feedback was submitted!",
"You may also:": "You may also:",
"You may now close this page or {return_to_the_homepage}.": "You may now close this page or {return_to_the_homepage}.",
"This group is a remote group, it's possible the original instance has more informations.": "This group is a remote group, it's possible the original instance has more informations.",
"View the group profile on the original instance": "View the group profile on the original instance",
"View past events": "View past events"
}

View File

@ -1309,5 +1309,8 @@
"Category": "Catégorie", "Category": "Catégorie",
"Select a category": "Choisissez une categorie", "Select a category": "Choisissez une categorie",
"Any category": "N'importe quelle catégorie", "Any category": "N'importe quelle catégorie",
"No instance found.": "Aucune instance trouvée." "No instance found.": "Aucune instance trouvée.",
"This group is a remote group, it's possible the original instance has more informations.": "Ce groupe est un groupe distant, il est possible que l'instance d'origine ait plus d'informations.",
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
"View past events": "Voir les événements passés"
} }

View File

@ -0,0 +1,50 @@
import {
IAnalyticsConfig,
IConfig,
IKeyValueConfig,
} from "@/types/config.model";
export const statistics = async (config: IConfig, environement: any) => {
console.debug("Loading statistics", config.analytics);
const matomoConfig = checkProviderConfig(config, "matomo");
if (matomoConfig?.enabled === true) {
const { matomo } = (await import("./matomo")) as any;
matomo(environement, convertConfig(matomoConfig.configuration));
}
const sentryConfig = checkProviderConfig(config, "sentry");
if (sentryConfig?.enabled === true) {
const { sentry } = (await import("./sentry")) as any;
sentry(environement, convertConfig(sentryConfig.configuration));
}
};
export const checkProviderConfig = (
config: IConfig,
providerName: string
): IAnalyticsConfig | undefined => {
return config?.analytics?.find((provider) => provider.id === providerName);
};
export const convertConfig = (
configs: IKeyValueConfig[]
): Record<string, any> => {
return configs.reduce((acc, config) => {
acc[config.key] = toType(config.value, config.type);
return acc;
}, {} as Record<string, any>);
};
const toType = (value: string, type: string): string | number | boolean => {
switch (type) {
case "boolean":
return value === "true";
case "integer":
return parseInt(value, 10);
case "float":
return parseFloat(value);
case "string":
default:
return value;
}
};

View File

@ -0,0 +1,14 @@
import Vue from "vue";
import VueMatomo from "vue-matomo";
export const matomo = (environment: any, matomoConfiguration: any) => {
console.debug("Loading Matomo statistics");
console.debug(
"Calling VueMatomo with the following configuration",
matomoConfiguration
);
Vue.use(VueMatomo, {
...matomoConfiguration,
router: environment.router,
});
};

View File

@ -0,0 +1,11 @@
import VueRouter from "vue-router";
import Vue from "vue";
import { VuePlausible } from "vue-plausible";
export default (router: VueRouter, plausibleConfiguration: any) => {
console.debug("Loading Plausible statistics");
Vue.use(VuePlausible, {
// see configuration section
...plausibleConfiguration,
});
};

View File

@ -0,0 +1,54 @@
import Vue from "vue";
import * as Sentry from "@sentry/vue";
import { Integrations } from "@sentry/tracing";
export const sentry = (environment: any, sentryConfiguration: any) => {
console.debug("Loading Sentry statistics");
console.debug(
"Calling Sentry with the following configuration",
sentryConfiguration
);
// Don't attach errors to previous events
window.sessionStorage.removeItem("lastEventId");
Sentry.init({
Vue,
dsn: sentryConfiguration.dsn,
integrations: [
new Integrations.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(
environment.router
),
tracingOrigins: ["localhost", "mobilizon1.com", /^\//],
}),
],
beforeSend(event) {
// Check if it is an exception, and if so, save it in session storage
// so that it can be retreived from the error component
if (event.exception && event.event_id) {
window.sessionStorage.setItem("lastEventId", event.event_id);
}
return event;
},
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: sentryConfiguration.tracesSampleRate,
release: environment.version,
});
};
export const submitFeedback = async (
endpoint: string,
dsn: string,
params: Record<string, string>
): Promise<void> => {
await fetch(endpoint, {
method: "POST",
headers: {
"Content-type": "application/json",
Authorization: `DSN ${dsn}`,
},
body: JSON.stringify(params),
});
};

View File

@ -0,0 +1,7 @@
export interface ISentryConfiguration {
dsn: string;
organization?: string;
project?: string;
host?: string;
tracesSampleRate: number;
}

View File

@ -6,6 +6,18 @@ export interface IOAuthProvider {
label: string; label: string;
} }
export interface IKeyValueConfig {
key: string;
value: string;
type: "boolean" | "integer" | "string";
}
export interface IAnalyticsConfig {
id: string;
enabled: boolean;
configuration: IKeyValueConfig[];
}
export interface IConfig { export interface IConfig {
name: string; name: string;
description: string; description: string;
@ -110,4 +122,5 @@ export interface IConfig {
exportFormats: { exportFormats: {
eventParticipants: string[]; eventParticipants: string[];
}; };
analytics: IAnalyticsConfig[];
} }

1
js/src/typings/matomo.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "vue-matomo";

View File

@ -43,21 +43,40 @@
<subtitle> <subtitle>
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }} {{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
</subtitle> </subtitle>
<b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch> <b-switch class="mb-4" v-model="showPassedEvents">{{
$t("Past events")
}}</b-switch>
<grouped-multi-event-minimalist-card <grouped-multi-event-minimalist-card
:events="group.organizedEvents.elements" :events="group.organizedEvents.elements"
:isCurrentActorMember="isCurrentActorMember" :isCurrentActorMember="isCurrentActorMember"
/> />
<b-message <empty-content
v-if=" v-if="
group.organizedEvents.elements.length === 0 && group.organizedEvents.elements.length === 0 &&
$apollo.loading === false $apollo.loading === false
" "
type="is-danger" icon="calendar"
:inline="true"
:center="true"
> >
{{ $t("No events found") }} {{ $t("No events found") }}
</b-message> <template v-if="group.domain !== null">
<div class="mt-4">
<p>
{{
$t(
"This group is a remote group, it's possible the original instance has more informations."
)
}}
</p>
<b-button type="is-text" tag="a" :href="group.url">
{{ $t("View the group profile on the original instance") }}
</b-button>
</div>
</template>
</empty-content>
<b-pagination <b-pagination
class="mt-4"
:total="group.organizedEvents.total" :total="group.organizedEvents.total"
v-model="eventsPage" v-model="eventsPage"
:per-page="EVENTS_PAGE_LIMIT" :per-page="EVENTS_PAGE_LIMIT"
@ -81,6 +100,7 @@ import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event"; import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { displayName, usernameWithDomain } from "../../types/actor"; import { displayName, usernameWithDomain } from "../../types/actor";
const EVENTS_PAGE_LIMIT = 10; const EVENTS_PAGE_LIMIT = 10;
@ -114,6 +134,7 @@ const EVENTS_PAGE_LIMIT = 10;
}, },
}, },
components: { components: {
EmptyContent,
Subtitle, Subtitle,
GroupedMultiEventMinimalistCard, GroupedMultiEventMinimalistCard,
}, },

View File

@ -579,29 +579,47 @@
</div> </div>
<empty-content v-else-if="group" icon="calendar" :inline="true"> <empty-content v-else-if="group" icon="calendar" :inline="true">
{{ $t("No public upcoming events") }} {{ $t("No public upcoming events") }}
<template #desc v-if="isCurrentActorFollowing"> <template #desc>
<i18n <template v-if="isCurrentActorFollowing">
class="has-text-grey-dark" <i18n
path="You will receive notifications about this group's public activity depending on %{notification_settings}." class="has-text-grey-dark"
> path="You will receive notifications about this group's public activity depending on %{notification_settings}."
<router-link
:to="{ name: RouteName.NOTIFICATIONS }"
slot="notification_settings"
>{{ $t("your notification settings") }}</router-link
> >
</i18n> <router-link
:to="{ name: RouteName.NOTIFICATIONS }"
slot="notification_settings"
>{{ $t("your notification settings") }}</router-link
>
</i18n>
</template>
<b-button
tag="router-link"
class="my-2"
type="is-text"
:to="{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
query: { future: false },
}"
>{{ $t("View past events") }}</b-button
>
</template> </template>
</empty-content> </empty-content>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <div class="flex justify-center">
v-if="organizedEvents.total > 0" <b-button
:to="{ tag="router-link"
name: RouteName.GROUP_EVENTS, class="my-4"
params: { preferredUsername: usernameWithDomain(group) }, type="is-text"
query: { future: organizedEvents.elements.length > 0 }, v-if="organizedEvents.total > 0"
}" :to="{
>{{ $t("View all events") }}</router-link name: RouteName.GROUP_EVENTS,
> params: { preferredUsername: usernameWithDomain(group) },
query: { future: organizedEvents.elements.length > 0 },
}"
>{{ $t("View all events") }}</b-button
>
</div>
</section> </section>
<section> <section>
<subtitle>{{ $t("Latest posts") }}</subtitle> <subtitle>{{ $t("Latest posts") }}</subtitle>

View File

@ -123,7 +123,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { ICurrentUser } from "@/types/current-user.model"; import { ICurrentUser } from "@/types/current-user.model";
import { LoginError, LoginErrorCode } from "@/types/enums"; import { LoginError, LoginErrorCode } from "@/types/enums";
@ -207,6 +207,11 @@ export default class Login extends Vue {
const { query } = this.$route; const { query } = this.$route;
this.errorCode = query.code as LoginErrorCode; this.errorCode = query.code as LoginErrorCode;
this.redirect = query.redirect as string | undefined; this.redirect = query.redirect as string | undefined;
// Already-logged-in and accessing /login
if (this.currentUser.isLoggedIn) {
this.$router.push("/");
}
} }
async loginAction(e: Event): Promise<Route | void> { async loginAction(e: Event): Promise<Route | void> {
@ -240,7 +245,7 @@ export default class Login extends Vue {
if (window.localStorage) { if (window.localStorage) {
window.localStorage.setItem("welcome-back", "yes"); window.localStorage.setItem("welcome-back", "yes");
} }
this.$router.push({ name: RouteName.HOME }); this.$router.replace({ name: RouteName.HOME });
return; return;
} catch (err: any) { } catch (err: any) {
this.submitted = false; this.submitted = false;
@ -279,13 +284,6 @@ export default class Login extends Vue {
} }
} }
@Watch("currentUser")
redirectToHomepageIfAlreadyLoggedIn(): Promise<Route> | void {
if (this.currentUser.isLoggedIn) {
return this.$router.push("/");
}
}
get hasCaseWarning(): boolean { get hasCaseWarning(): boolean {
return this.credentials.email !== this.credentials.email.toLowerCase(); return this.credentials.email !== this.credentials.email.toLowerCase();
} }

View File

@ -13,6 +13,7 @@ export const defaultResolvers = {
id: "67", id: "67",
preferredUsername: "someone", preferredUsername: "someone",
name: "Personne", name: "Personne",
avatar: null,
__typename: "CurrentActor", __typename: "CurrentActor",
}), }),
}, },

View File

@ -22,7 +22,7 @@ import { InMemoryCache } from "@apollo/client/cache";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
config.mocks.$t = (key: string): string => key; config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn() }; const $router = { push: jest.fn(), replace: jest.fn() };
describe("Render login form", () => { describe("Render login form", () => {
let wrapper: Wrapper<Vue>; let wrapper: Wrapper<Vue>;
@ -125,9 +125,9 @@ describe("Render login form", () => {
await flushPromises(); await flushPromises();
expect(currentUser?.email).toBe("some@email.tld"); expect(currentUser?.email).toBe("some@email.tld");
expect(currentUser?.id).toBe("1"); expect(currentUser?.id).toBe("1");
expect(jest.isMockFunction(wrapper.vm.$router.push)).toBe(true); expect(jest.isMockFunction(wrapper.vm.$router.replace)).toBe(true);
await flushPromises(); await flushPromises();
expect($router.push).toHaveBeenCalledWith({ name: RouteName.HOME }); expect($router.replace).toHaveBeenCalledWith({ name: RouteName.HOME });
}); });
it("handles a login error", async () => { it("handles a login error", async () => {

View File

@ -123,6 +123,8 @@ export const configMock = {
enabled: true, enabled: true,
publicKey: "", publicKey: "",
}, },
eventCategories: [],
analytics: [],
}, },
}, },
}; };

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Categories
alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Medias.Media alias Mobilizon.Medias.Media
@ -73,7 +74,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
medias: medias, medias: medias,
begins_on: object["startTime"], begins_on: object["startTime"],
ends_on: object["endTime"], ends_on: object["endTime"],
category: object["category"], category: get_category(object["category"]),
visibility: visibility, visibility: visibility,
join_options: Map.get(object, "joinMode", "free"), join_options: Map.get(object, "joinMode", "free"),
local: is_local?(object["id"]), local: is_local?(object["id"]),
@ -330,4 +331,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
_participant_count _participant_count
), ),
do: nil do: nil
@spec get_category(String.t() | nil) :: String.t()
defp get_category(nil), do: "MEETING"
defp get_category(category) when is_binary(category) do
if category in Enum.map(Categories.list(), &String.upcase(to_string(&1.id))) do
category
else
get_category(nil)
end
end
end end

View File

@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Events.Categories alias Mobilizon.Events.Categories
alias Mobilizon.Service.FrontEndAnalytics
@doc """ @doc """
Gets config. Gets config.
@ -170,7 +171,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
public_key: public_key:
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key]) get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key])
}, },
export_formats: Config.instance_export_formats() export_formats: Config.instance_export_formats(),
analytics: FrontEndAnalytics.config()
} }
end end
end end

View File

@ -75,6 +75,10 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:web_push, :web_push, description: "Web Push settings for the instance") field(:web_push, :web_push, description: "Web Push settings for the instance")
field(:export_formats, :export_formats, description: "The instance list of export formats") field(:export_formats, :export_formats, description: "The instance list of export formats")
field(:analytics, list_of(:analytics),
description: "Configuration for diverse analytics services"
)
end end
@desc """ @desc """
@ -330,6 +334,28 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:public_key, :string, description: "The server's public WebPush VAPID key") field(:public_key, :string, description: "The server's public WebPush VAPID key")
end end
object :analytics do
field(:id, :string, description: "ID of the analytics service")
field(:enabled, :boolean, description: "Whether the service is activated or not")
field(:configuration, list_of(:analytics_configuration),
description: "A list of key-values configuration"
)
end
enum :analytics_configuration_type do
value(:string, description: "A string")
value(:integer, description: "An integer")
value(:boolean, description: "A boolean")
value(:float, description: "A float")
end
object :analytics_configuration do
field(:key, :string, description: "The key for the analytics configuration element")
field(:value, :string, description: "The value for the analytics configuration element")
field(:type, :analytics_configuration_type, description: "The analytics configuration type")
end
@desc """ @desc """
Export formats configuration Export formats configuration
""" """

View File

@ -0,0 +1,71 @@
defmodule Mobilizon.Service.FrontEndAnalytics do
@moduledoc """
Behaviour for any analytics service
"""
@callback id() :: String.t()
@doc """
Whether the service is enabled
"""
@callback enabled?() :: boolean()
@doc """
The configuration for the service
"""
@callback configuration() :: keyword()
@doc """
The CSP configuration to add for the service to work
"""
@callback csp() :: keyword()
@spec providers :: list(module())
def providers do
:mobilizon
|> Application.get_env(:analytics, [])
|> Keyword.get(:providers, [])
end
@spec config :: map()
def config do
Enum.reduce(providers(), [], &load_config/2)
end
@spec csp :: keyword()
def csp do
providers()
|> Enum.map(& &1.csp())
|> Enum.reduce([], &merge_csp_config/2)
end
@spec load_config(module(), list(map())) :: list(map())
defp load_config(provider, acc) do
acc ++
[
%{
id: provider.id(),
enabled: provider.enabled?(),
configuration: convert_config(provider.configuration())
}
]
end
@spec convert_config(Keyword.t()) :: list(map())
defp convert_config(config) do
Enum.reduce(config, [], fn {key, val}, acc ->
acc ++ [%{key: key, value: val, type: type(val)}]
end)
end
defp type(val) when is_integer(val), do: :integer
defp type(val) when is_float(val), do: :float
defp type(val) when is_boolean(val), do: :boolean
defp type(val) when is_binary(val), do: :string
defp merge_csp_config(config, global_config) do
Keyword.merge(global_config, config, fn _key, global, config ->
"#{global} #{config}"
end)
end
end

View File

@ -0,0 +1,40 @@
defmodule Mobilizon.Service.FrontEndAnalytics.Matomo do
@moduledoc """
Matomo analytics provider
"""
alias Mobilizon.Service.FrontEndAnalytics
@behaviour FrontEndAnalytics
@impl FrontEndAnalytics
def id, do: "matomo"
@doc """
Whether the service is enabled
"""
@impl FrontEndAnalytics
def enabled? do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:enabled, false)
end
@doc """
The configuration for the service
"""
@impl FrontEndAnalytics
def configuration do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.drop([:enabled, :csp])
end
@doc """
The CSP configuration to add for the service to work
"""
@impl FrontEndAnalytics
def csp do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:csp, [])
end
end

View File

@ -0,0 +1,41 @@
defmodule Mobilizon.Service.FrontEndAnalytics.Plausible do
@moduledoc """
Plausible analytics provider
"""
alias Mobilizon.Service.FrontEndAnalytics
@behaviour FrontEndAnalytics
@impl FrontEndAnalytics
def id, do: "plausible"
@doc """
Whether the service is enabled
"""
@impl FrontEndAnalytics
def enabled? do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:enabled, false)
end
@doc """
The configuration for the service
"""
@impl FrontEndAnalytics
def configuration do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.drop([:enabled, :csp])
end
@doc """
The CSP configuration to add for the service to work
"""
@impl FrontEndAnalytics
def csp do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:csp, [])
end
end

View File

@ -0,0 +1,41 @@
defmodule Mobilizon.Service.FrontEndAnalytics.Sentry do
@moduledoc """
Sentry analytics provider
"""
alias Mobilizon.Service.FrontEndAnalytics
@behaviour FrontEndAnalytics
@impl FrontEndAnalytics
def id, do: "sentry"
@doc """
Whether the service is enabled
"""
@impl FrontEndAnalytics
def enabled? do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:enabled, false)
end
@doc """
The configuration for the service
"""
@impl FrontEndAnalytics
def configuration do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.drop([:enabled, :csp])
end
@doc """
The CSP configuration to add for the service to work
"""
@impl FrontEndAnalytics
def csp do
:mobilizon
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:csp, [])
end
end

View File

@ -29,9 +29,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
@spec do_get_actor(String.t()) :: {:commit, Actor.t()} | {:ignore, nil} @spec do_get_actor(String.t()) :: {:commit, Actor.t()} | {:ignore, nil}
defp do_get_actor("actor_" <> name) do defp do_get_actor("actor_" <> name) do
case Actor.find_or_make_actor_from_nickname(name) do case Actor.find_or_make_actor_from_nickname(name) do
{:ok, %ActorModel{} = actor} -> {:ok, %ActorModel{suspended: false} = actor} ->
{:commit, actor} {:commit, actor}
{:ok, %ActorModel{}} ->
{:ignore, nil}
{:error, _err} -> {:error, _err} ->
{:ignore, nil} {:ignore, nil}
end end
@ -45,9 +48,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
def get_local_actor_by_name(name) do def get_local_actor_by_name(name) do
Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name -> Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name ->
case Actors.get_local_actor_by_name(name) do case Actors.get_local_actor_by_name(name) do
%ActorModel{} = actor -> %ActorModel{suspended: false} = actor ->
{:commit, actor} {:commit, actor}
{:ok, %ActorModel{}} ->
{:ignore, nil}
nil -> nil ->
{:ignore, nil} {:ignore, nil}
end end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
""" """
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Service.FrontEndAnalytics
import Plug.Conn import Plug.Conn
require Logger require Logger
@ -136,8 +137,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
@spec get_csp_config(atom(), Keyword.t()) :: iodata() @spec get_csp_config(atom(), Keyword.t()) :: iodata()
defp get_csp_config(type, options) do defp get_csp_config(type, options) do
options config_policy = Keyword.get(options, type, Config.get([:http_security, :csp_policy, type]))
|> Keyword.get(type, Config.get([:http_security, :csp_policy, type])) front_end_analytics_policy = [Keyword.get(FrontEndAnalytics.csp(), type, [])]
|> Enum.join(" ")
Enum.join(config_policy ++ front_end_analytics_policy, " ")
end end
end end

View File

@ -168,7 +168,7 @@ defmodule Mobilizon.Mixfile do
{:mogrify, "~> 0.9"}, {:mogrify, "~> 0.9"},
{:linkify, "~> 0.3"}, {:linkify, "~> 0.3"},
{:http_signatures, "~> 0.1.0"}, {:http_signatures, "~> 0.1.0"},
{:ex_cldr, "~> 2.0"}, {:ex_cldr, "2.27.1"},
{:ex_cldr_dates_times, "~> 2.2"}, {:ex_cldr_dates_times, "~> 2.2"},
{:ex_optimizer, "~> 0.1"}, {:ex_optimizer, "~> 0.1"},
{:progress_bar, "~> 2.0"}, {:progress_bar, "~> 2.0"},

View File

@ -103,7 +103,7 @@
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.0", "5ea4036a3c8f372e6fbf928c822b16028bcaaf2b26ea83d5775670498af7bd92", [:mix], [], "hexpm", "fe61113eff12693a758080ac595dc86bfe3744d4734520a96f6c1a0d7f13c126"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.1", "0db6eb6405a6b06cae4fdf4144659b3f4fee4553e2856fe8a53ba12e9fb21a74", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e34890004baec08f0fa12bd8c77bf64bfb4156b84a07fb79da9322fa94bc3781"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.1", "0db6eb6405a6b06cae4fdf4144659b3f4fee4553e2856fe8a53ba12e9fb21a74", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e34890004baec08f0fa12bd8c77bf64bfb4156b84a07fb79da9322fa94bc3781"},
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
"plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"}, "plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"},
@ -122,7 +122,7 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"}, "struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"},
"sweet_xml": {:hex, :sweet_xml, "0.7.2", "4729f997286811fabdd8288f8474e0840a76573051062f066c4b597e76f14f9f", [:mix], [], "hexpm", "6894e68a120f454534d99045ea3325f7740ea71260bc315f82e29731d570a6e8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.2", "4729f997286811fabdd8288f8474e0840a76573051062f066c4b597e76f14f9f", [:mix], [], "hexpm", "6894e68a120f454534d99045ea3325f7740ea71260bc315f82e29731d570a6e8"},
"swoosh": {:hex, :swoosh, "1.6.3", "598d3f07641004bedb3eede40057760ae18be1073cff72f079ca1e1fc9cd97b9", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "81ff9d7c7c4005a57465a7eb712edd71db51829aef94c8a34c30c5b9e9964adf"}, "swoosh": {:hex, :swoosh, "1.6.4", "ce3a4bf3e5276fd114178ebc5ed072ee0c177a7b3a09e5992aa005778ac143c2", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad4c8b534812433730b6241a1d9df38b1da75fdfa340f51887a31d7e9343fffe"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"}, "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},

View File

@ -14,6 +14,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Service.ActorSuspension
alias Mobilizon.Service.HTTP.ActivityPub.Mock alias Mobilizon.Service.HTTP.ActivityPub.Mock
alias Mobilizon.Web.ActivityPub.ActorView alias Mobilizon.Web.ActivityPub.ActorView
alias Mobilizon.Web.{Endpoint, PageView} alias Mobilizon.Web.{Endpoint, PageView}
@ -44,6 +45,24 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
|> Jason.encode!() |> Jason.encode!()
|> Jason.decode!() |> Jason.decode!()
end end
test "it returns nothing if the actor is suspended", %{conn: conn} do
suspended = insert(:actor)
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
assert json_response(conn, 200)
assert {:ok, true} ==
Cachex.exists?(:activity_pub, "actor_" <> suspended.preferred_username)
ActorSuspension.suspend_actor(suspended)
assert {:ok, false} ==
Cachex.exists?(:activity_pub, "actor_" <> suspended.preferred_username)
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
assert json_response(conn, 404)
end
end end
describe "/events/:uuid" do describe "/events/:uuid" do

View File

@ -4,7 +4,7 @@ defmodule Mobilizon.Web.PageControllerTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActorSuspension
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
@ -37,6 +37,18 @@ defmodule Mobilizon.Web.PageControllerTest do
conn = get(conn, Actor.build_url("not_existing", :page)) conn = get(conn, Actor.build_url("not_existing", :page))
assert html_response(conn, 404) assert html_response(conn, 404)
end end
test "GET /@actor when suspended", %{conn: conn} do
suspended = insert(:actor)
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
assert html_response(conn, 200)
ActorSuspension.suspend_actor(suspended)
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
assert html_response(conn, 404)
end
end end
test "GET /events/:uuid", %{conn: conn} do test "GET /events/:uuid", %{conn: conn} do

View File

@ -73,7 +73,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlugTest do
[csp] = Conn.get_resp_header(conn, "content-security-policy") [csp] = Conn.get_resp_header(conn, "content-security-policy")
assert csp =~ assert csp =~
~r/script-src 'self' 'unsafe-eval' 'sha256-[\w+\/=]*' example.com matomo.example.com;/ ~r/script-src 'self' 'unsafe-eval' 'sha256-[\w+\/=]*' example.com matomo.example.com ;/
end end
end end