diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a1b2e64a..1883f8ef0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -306,7 +306,7 @@ docker-latest: # Packaging app for amd64 package-app: - image: mobilizon/buildpack:1.15.7-erlang-26.1.2-${SYSTEM} + image: mobilizon/buildpack:1.16.1-erlang-26.2.2-${SYSTEM} stage: build variables: &release-variables MIX_ENV: "prod" @@ -340,7 +340,6 @@ package-app: "debian-buster", "ubuntu-jammy", "ubuntu-focal", - "ubuntu-bionic", "fedora-38", "fedora-39", ] diff --git a/.sobelow-skips b/.sobelow-skips index b0d6ee102..881741d19 100644 --- a/.sobelow-skips +++ b/.sobelow-skips @@ -1,20 +1,14 @@ -155A1FB53DE39EC8EFCFD7FB94EA823D -1C29EE70E90ECED01AF28EC58D2575B5 26ED12A8E03D044BEDC08749BAA5E357 -2BB1D36656B423758A470021718FCB09 31CE26BC979C57B9E3CC97B40C290CE5 -3529E7A4CECC24D02678820E6F521162 -3644C4E850300482AA409471EFE1EFB3 4E7C044C59E0BCB76AA826789998F624 -53CBBEB6243FAF5C37249CBA17DE6F4C 5BCE3651A03711295046DE48BDFE007E 5C4CED447689F00D9D1ACEB9B895ED29 +5D8350E7A2DE5BB1CBCC983AF121F0DA 94ACF7B17C3FF42F64E57DD1DA936BD8 A32E125003F1EDFAD95C487C6A969725 -ACF6272A1DBB3A2ABD96C0C120B5CA69 C46C4893B2F702ACADC4CAA5683FE370 -CDF2CCE0CF10F49CDFAE22FE26208155 +CD4CD6571816FCAF1E305066CDA0CD2F E720CB13C50FF3ADEE7C522531E11217 F3D5851D3FB050939841ED2F14307A27 FD1C9756370A195B74E95CE504C45E9E \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 1c43fd8c8..91256ffc0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ erlang 26.2.2 elixir 1.16.1-otp-26 +nodejs 18.19.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 657d80bde..7aaaac264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,105 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.1.0 (2024-02-29) + +This release is the last provided by Framasoft. [The project is now supported by the Kaihuri association](https://framacolibri.org/t/mobilizon-nous/19752). + +The highlights for this release are the following: + - improved event federation with https://event-federation.eu and https://gancio.org, as well as adding event metadata in summary for micro-blogging platforms that make use of it like Mastodon + - allowing to filter events by local-only, so that you can find only the events created on this instance + + +### Features + +* **activitpub:** add summary of metadata to events ([1441d35](https://framagit.org/framasoft/mobilizon/commits/1441d35e0b0c3a151b8626711b3acaf30d3dcf58)) +* **activitypub:** allow simple text for address field ([64237cf](https://framagit.org/framasoft/mobilizon/commits/64237cfc2633794a48022a059c79155b1ece14d1)), closes [#1387](https://framagit.org/framasoft/mobilizon/issues/1387) +* **activitypub:** implement FEP-2677 to identify the application actor used for federation ([f10977a](https://framagit.org/framasoft/mobilizon/commits/f10977a99ac73ce5a702a12ed31e773a4b0f6961)), closes [#1367](https://framagit.org/framasoft/mobilizon/issues/1367) +* allow to filter events by local-only ([9d99684](https://framagit.org/framasoft/mobilizon/commits/9d996844025f9d128305a54f8f169fb4b1ffac44)), closes [#1322](https://framagit.org/framasoft/mobilizon/issues/1322) +* **config:** enable instance feeds by default ([ab3f5df](https://framagit.org/framasoft/mobilizon/commits/ab3f5dfd278dc55dd0f28bb11cf4a976d212e9fc)) +* **docker:** add new environment variables for Docker config ([28430d6](https://framagit.org/framasoft/mobilizon/commits/28430d6d57a85b568c839e75ba1bcbff90e4149e)) +* **front:** upgrade to Oruga 0.8.x ([a9676d6](https://framagit.org/framasoft/mobilizon/commits/a9676d6481e6966d939ea4e44ad610eb9231c370)) +* **graphql:** increase max_complexity to 300 ([dcbb8ea](https://framagit.org/framasoft/mobilizon/commits/dcbb8eae01012e6e3aa2c83e06cc50f61176b8ef)) +* **http:** allow to provide self-signed certificates ([baa11c1](https://framagit.org/framasoft/mobilizon/commits/baa11c18b03684e508e56793a800878e95644962)), closes [#1355](https://framagit.org/framasoft/mobilizon/issues/1355) +* **nodeinfo:** extract and save NodeInfo information from instances to display it on instances list ([99b2339](https://framagit.org/framasoft/mobilizon/commits/99b2339424edb5b0c514581fbd6a42e4f0fcc5e1)), closes [#1392](https://framagit.org/framasoft/mobilizon/issues/1392) + +### Bug Fixes + +* **activitypub:** also handle as:Public and Public values for public addressing ([4dc2f48](https://framagit.org/framasoft/mobilizon/commits/4dc2f489e79d4f7d64ba3d5c2588d5d6ec0bc99c)), closes [#1413](https://framagit.org/framasoft/mobilizon/issues/1413) +* **activitypub:** consider PM as private conversations even if attributed_to_id is defined ([387d3b1](https://framagit.org/framasoft/mobilizon/commits/387d3b1c30ec719a992c565fd495ef2b6642641e)) +* **activitypub:** do not try to calculate timezone from missing geo-coordinates ([001a0ed](https://framagit.org/framasoft/mobilizon/commits/001a0ed1a50894ad1abe0f7fc9dc5b9666de5dae)) +* **activitypub:** handle actors following with manually_approves_followers not set ([7351468](https://framagit.org/framasoft/mobilizon/commits/73514688423b92b9f62b52fd54f2e523abeb3e34)) +* **activitypub:** handle any type of error when fetching Application actor from NodeInfo ([9308c53](https://framagit.org/framasoft/mobilizon/commits/9308c5399dc2e7afd8844d083de2ea291a3c5a66)) +* **activitypub:** handle issue with AP Fetcher not catching some changeset errors ([e3b3643](https://framagit.org/framasoft/mobilizon/commits/e3b36434cb05feb2e6add2b6b229e83b9dccf825)), closes [#1409](https://framagit.org/framasoft/mobilizon/issues/1409) +* **activitypub:** make relay outbox events ordered by desc publication date ([e73fd9b](https://framagit.org/framasoft/mobilizon/commits/e73fd9b370b9679a0ab424a0bd44f262a21a4697)) +* **activitypub:** refresh NodeInfo metadata straight away when adding a new instance to follow ([2f4b8fe](https://framagit.org/framasoft/mobilizon/commits/2f4b8feeba9e7e1c4d1fc967505b3ed80e314b3c)) +* allow html_to_text to receive nil, e.g. for empty event descriptions ([5030b75](https://framagit.org/framasoft/mobilizon/commits/5030b755a0880a022d0656598b591cb47ebd7dc5)) +* **announcements:** error message not showing when an event announcement is created with empty text ([ef20585](https://framagit.org/framasoft/mobilizon/commits/ef20585f8cc1e4ac2f2f3359a70b7f456d2adeeb)) +* **announcements:** make sure only valid announcements are shown to the user ([c9a1c35](https://framagit.org/framasoft/mobilizon/commits/c9a1c35aa7a1d399b524dc5cc1fbebb38681ee24)) +* **backend:** avoid duplicating locality and region if they are the same ([5de22f9](https://framagit.org/framasoft/mobilizon/commits/5de22f91e22109da9e2169928dc744acd94b7299)) +* **backend:** fix sending N notifications to a single conversation participant ([9537988](https://framagit.org/framasoft/mobilizon/commits/95379885c8fb3decd19fa434774023a7b05ef0b5)), closes [#1384](https://framagit.org/framasoft/mobilizon/issues/1384) +* **backend:** hide non-public replies to comments in event comment threads ([10c4038](https://framagit.org/framasoft/mobilizon/commits/10c4038b856b7e5c4981dcdce0bb9a885afb3cea)) +* **backend:** only send announcement event emails when the comment author has the right to do so ([0bd00de](https://framagit.org/framasoft/mobilizon/commits/0bd00de501b36c5f2320c2530019f302bf084517)) +* **backend:** validate length of instance actor details and set description column to text ([f7585cf](https://framagit.org/framasoft/mobilizon/commits/f7585cfc759576475133bcc86d2e816b2553626d)), closes [#1393](https://framagit.org/framasoft/mobilizon/issues/1393) +* **back:** fix instances filtering ([b3ba45e](https://framagit.org/framasoft/mobilizon/commits/b3ba45e8a73038dc70286afbb479c1db51b6fbcd)) +* **back:** sitemapper fix after upgrade ([1acf931](https://framagit.org/framasoft/mobilizon/commits/1acf931ac558ac0818213264a6177a1f647393f1)) +* **docker:** add --break-system-packages to pip install to add weasyprint and pyexcel-ods3 ([889cb91](https://framagit.org/framasoft/mobilizon/commits/889cb91f2649861a87eb7e959065cfb49b30f366)) +* **docker:** remove openssl1.1-compat ([75d7816](https://framagit.org/framasoft/mobilizon/commits/75d7816a6cd1fe6754a66c1bb81153068b9c13e3)), closes [#1390](https://framagit.org/framasoft/mobilizon/issues/1390) +* **event announcements:** only show comments from event organizers in event announcement list ([01eecbf](https://framagit.org/framasoft/mobilizon/commits/01eecbf1d46614241c92e1a38e30057a84c55744)) +* **feeds:** increase feed item limit from 500 to 5000 ([ff0440c](https://framagit.org/framasoft/mobilizon/commits/ff0440c634ac17813607f5929cd4024d87601c3b)) +* **feeds:** make sure posts for feeds are ordered by publication date desc ([3c75856](https://framagit.org/framasoft/mobilizon/commits/3c7585614971849035011ede6c0d5d2d5621df81)) +* **front-end:** fix current actor not being set on first access when relogging ([ae466b8](https://framagit.org/framasoft/mobilizon/commits/ae466b879cd09a9d04ffab0469ee991c7d90ce8e)) +* **front-end:** fix issues with expired accessToken refreshment queue ([d4489f6](https://framagit.org/framasoft/mobilizon/commits/d4489f691b312891013767f7e39d92a9b0863387)) +* **front:** add a required attribute to the text editor and show error message if text empty on blur ([ba66874](https://framagit.org/framasoft/mobilizon/commits/ba66874cc3e5979c2a9a6f86ea55463eca911472)) +* **front:** add announcements link on EventParticipationCard as well as EventView ([83eb5c6](https://framagit.org/framasoft/mobilizon/commits/83eb5c6a69ac312c19dc3cef10f26ab686cb4be7)) +* **front:** add condition on DraggableList in ResourceFolder.vue ([a408b47](https://framagit.org/framasoft/mobilizon/commits/a408b476cf2151298c7cf4eb6b3268334be13599)) +* **front:** correctly show error message when a tag is too short ([cba2075](https://framagit.org/framasoft/mobilizon/commits/cba2075431d1de4bf621e1d2b2a2e5f0641997c6)), closes [#1382](https://framagit.org/framasoft/mobilizon/issues/1382) +* **front:** create head without old options ([45f8757](https://framagit.org/framasoft/mobilizon/commits/45f8757d72d1a2c72d069ced6fcbe21571d334c5)) +* **frontend:** various fixes ([456dc36](https://framagit.org/framasoft/mobilizon/commits/456dc36f64b3eb7c43d8ff69aa458b89b5a5b4ab)) +* **front:** escape event.title when it's passed to dialog component HTML message ([f4ee116](https://framagit.org/framasoft/mobilizon/commits/f4ee11611294c2cc957453768f768de0a51b05a7)) +* **front:** fix debouncing instances filtering ([fe0cf93](https://framagit.org/framasoft/mobilizon/commits/fe0cf9360428185d261dad4065a7bea1dd8d8d59)) +* **front:** fix dialog from EventParticipationCard.vue without input ([89641c5](https://framagit.org/framasoft/mobilizon/commits/89641c502ef5771f93cfa55caea6b52c63e73b4b)) +* **front:** fix ErrorComponent.vue sentry integration ([00d8bc7](https://framagit.org/framasoft/mobilizon/commits/00d8bc733d52a810c438e1081496e3b0ac58958f)) +* **front:** fix focus when creating a new resource ([76668e0](https://framagit.org/framasoft/mobilizon/commits/76668e0bebd2bd235925494f90fac6400e74d179)) +* **front:** fix focusing text editor ([3b7124a](https://framagit.org/framasoft/mobilizon/commits/3b7124a57b2dedf5583fdebced6b9a4e502e8731)) +* **front:** fix reporting group ([57d0372](https://framagit.org/framasoft/mobilizon/commits/57d0372ce8b29952caff8bbf7c902c7862a77b49)) +* **front:** fix TagInput display ([790db90](https://framagit.org/framasoft/mobilizon/commits/790db906a6e814352aa694c26febb9d6a43fa321)) +* **front:** fix TagInput width properly ([6a4123f](https://framagit.org/framasoft/mobilizon/commits/6a4123f385fb2e20aab1c1cbc666c5d1a3f93589)) +* **front:** husky fixes after upgrade ([04edc4f](https://framagit.org/framasoft/mobilizon/commits/04edc4fef08306c55067abd0e22443c4cb43d7c8)) +* **front:** improve display of SendPasswordReset view ([1d39eb5](https://framagit.org/framasoft/mobilizon/commits/1d39eb548898b3c4840b4a36950a62b4ce46ba90)) +* **front:** only update identity username from name if it's a new identity ([34c0dd6](https://framagit.org/framasoft/mobilizon/commits/34c0dd6498247cf6a90576a602c4e305c80c9692)) +* **front:** patch vue-i18n-extract because of mjs incompatibility ([1f4a7c2](https://framagit.org/framasoft/mobilizon/commits/1f4a7c253bfe40809b432f3a36faa6b5fb340ae9)) +* **front:** remove broken identity check in EventMinimalistCard ([ee63814](https://framagit.org/framasoft/mobilizon/commits/ee6381463d9f8e6d130e29b410cf5e2700f3c10b)) +* **front:** reset instances list to page 1 if filter or follow status changes ([2b5439b](https://framagit.org/framasoft/mobilizon/commits/2b5439b1d0ef1f60c19019540a01eb6d437eee23)) +* **front:** reset page to lower or page 1 if we didn't found results in instances view ([48f57ec](https://framagit.org/framasoft/mobilizon/commits/48f57ec1cf3ce81c3c83333bea59c2a7d8c70e99)) +* **front:** rollback to vue 3.3 for now ([5cb4fc1](https://framagit.org/framasoft/mobilizon/commits/5cb4fc11c4ccc381a041cb2615913a8fb77e1b85)) +* **front:** show correct label when adding a new calc or videoconference resource in resources ([cecbea6](https://framagit.org/framasoft/mobilizon/commits/cecbea6db52d360e046d69cf0762eb1208c45f19)) +* **front:** tagInput fixes ([f6bcb02](https://framagit.org/framasoft/mobilizon/commits/f6bcb02b9802e04bd8e9c80092a0680b64482688)) +* **front:** uI fixes ([0948cce](https://framagit.org/framasoft/mobilizon/commits/0948cce83e5af128f78b67891ed24c323b159f0f)) +* **front:** use functions to generate classnames dynamically ([98230a5](https://framagit.org/framasoft/mobilizon/commits/98230a56bb5e1c75f070e4d4c352028741869066)) +* **front:** various cleanups ([6a482b0](https://framagit.org/framasoft/mobilizon/commits/6a482b0d9754fc85f1f61922e92852fbca52beb9)) +* **front:** various little CSS fixes ([51d43aa](https://framagit.org/framasoft/mobilizon/commits/51d43aa2d1d1f099078895d67a45fc27b74d4604)) +* **front:** various UI improvements ([a6a1ab7](https://framagit.org/framasoft/mobilizon/commits/a6a1ab71c23264805d61b5312982e6d345454027)) +* **front:** vite fixes after upgrade (everything is esm) ([b1ecf4b](https://framagit.org/framasoft/mobilizon/commits/b1ecf4b36f5855c895f72c4d9dc0f7e1beb449e1)) +* **graphql:** add missing operation name for RegisterPerson ([a47f4f6](https://framagit.org/framasoft/mobilizon/commits/a47f4f6444d12a13a6f7e79ed6746e74088ca294)) +* **graphql:** fix checking actor identity when publishing event announcements ([5bc0593](https://framagit.org/framasoft/mobilizon/commits/5bc0593ed6e772d48722c308ccb444dc49f3c079)) +* **nodeinfo:** fix getting application actor information from NodeInfo response ([dd775b6](https://framagit.org/framasoft/mobilizon/commits/dd775b6ae25f381cf76e00999fd7d37764870122)) +* **nodeinfo:** make sure we only process JSON content ([da3b074](https://framagit.org/framasoft/mobilizon/commits/da3b0746198544d7977d9c0b32d8a26e1da64d40)) +* **front:** fix adding tags to an event ([d75d464](https://framagit.org/framasoft/mobilizon/commits/d75d464135332f639bd275684109a89980718b75)), closes [#1419](https://framagit.org/framasoft/mobilizon/issues/1419) +* **front:** fix space around input icons ([ba9299c](https://framagit.org/framasoft/mobilizon/commits/ba9299c6321cd62bb84efb6db5b6e122e4b1b264)) +* **backend:** set Gettext default locale to "en" ([d390a91](https://framagit.org/framasoft/mobilizon/commits/d390a915d80ce5d2447f5323b78c71e9e1aa58dc)) +* **front:** fix discussion comment changed subscription done before having slug value ([0670297](https://framagit.org/framasoft/mobilizon/commits/067029705dd3c78b54ea4765357ba58930144aab)) +* **front:** fix typing for canReport prop on DiscussionComment ([c2055d9](https://framagit.org/framasoft/mobilizon/commits/c2055d92ae7707b5aab3fd14ea827df0696cca61)) +* **front:** remove extra classes on comment that are not needed ([a9b9775](https://framagit.org/framasoft/mobilizon/commits/a9b977540b900416cfe0d5739cba13e506f83120)) + +## 4.1.0-beta.1 (2024-02-27) + +### Bug Fixes + +* **front:** fix adding tags to an event ([d75d464](https://framagit.org/framasoft/mobilizon/commits/d75d464135332f639bd275684109a89980718b75)), closes [#1419](https://framagit.org/framasoft/mobilizon/issues/1419) +* **front:** fix space around input icons ([ba9299c](https://framagit.org/framasoft/mobilizon/commits/ba9299c6321cd62bb84efb6db5b6e122e4b1b264)) + + ## 4.1.0-alpha.1 (2024-02-09) ### Features diff --git a/LICENSE b/LICENSE index 58db0a43d..db9fb08d8 100644 --- a/LICENSE +++ b/LICENSE @@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Mobilizon - Copyright (C) 2018 Thomas Citharel + Copyright (C) 2018 - 2024 Framasoft This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/Makefile b/Makefile index c4be98db4..26322ad7c 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,8 @@ stop: @bash docker/message.sh "Mobilizon is stopped" test: stop @bash docker/message.sh "Running tests" - docker compose -f docker compose.yml -f docker compose.test.yml run api mix prepare_test - docker compose -f docker compose.yml -f docker compose.test.yml run api mix test $(only) + docker compose -f docker-compose.yml -f docker-compose.test.yml run api mix prepare_test + docker compose -f docker-compose.yml -f docker-compose.test.yml run api mix test $(only) @bash docker/message.sh "Done running tests" format: docker compose run --rm api bash -c "mix format && mix credo --strict" diff --git a/README.md b/README.md index fa3bbf7be..5d84cbe6f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Mobilizon is your federated organization and mobilization platform. Gather people with a convivial, ethical, and emancipating tool.

- Developed with ♥ by Framasoft + 2017 - 2024 Developed with ♥ by Framasoft

diff --git a/SECURITY.md b/SECURITY.md index be9788c4d..6c9ab1d37 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,7 @@ [Mobilizon](https://joinmobilizon.org) takes security, privacy and user control seriously, and we want to put them front and centre of our project. This document outlines security procedures and general policies for the Mobilizon project. -Framasoft, the Mobilizon maintainer team and community take all security bugs in Mobilizon seriously. Thank you for improving the security of Mobilizon. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. +The Mobilizon maintainer team and community take all security bugs in Mobilizon seriously. Thank you for improving the security of Mobilizon. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. ### Goals @@ -15,8 +15,6 @@ Framasoft, the Mobilizon maintainer team and community take all security bugs in * GDPR compliance. -Framasoft is both a developer of open-source/free/libre self-hosted software, and a service provider with users in the European Union. As a result, we are putting user privacy, data sovereignty, and GDPR compliance into our security plans, including asking both the Framasoft community and outside hackers to review our approaches and implementations. - ### Challenges [Mobilizon](https://joinmobilizon.org) will be challenging to keep secure, as it is: @@ -33,14 +31,14 @@ This means there are more attack surfaces compared to typical proprietary, centr We are committed to working with security researchers to verify, reproduce, and respond to legitimate reported vulnerabilities. You can help us by following these simple guidelines: -* Alert us about the vulnerability as soon as you become aware of it by emailing the lead maintainer at tcit+mobilizon@framasoft.org. +* Alert us about the vulnerability as soon as you become aware of it by emailing the lead maintainer. * Provide details needed to reproduce and validate the vulnerability and a Proof of Concept (PoC) as soon as possible * Act in good faith to avoid privacy violations, destruction of data, and interruption or degradation of services * Do not access or modify users’ private data, without explicit permission of the owner. Only interact with your own accounts or test accounts for security research purposes; -* Contact Framasoft or a maintainer of the Mobilizon project (or the instance admin) immediately if you do inadvertently encounter user data. Do not view, alter, save, store, transfer, or otherwise access the data, and immediately purge any local information upon reporting the vulnerability; +* Contact a maintainer of the Mobilizon project (or the instance admin) immediately if you do inadvertently encounter user data. Do not view, alter, save, store, transfer, or otherwise access the data, and immediately purge any local information upon reporting the vulnerability; * The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. * Give us time to confirm, determine the affected versions and prepare fixes to correct the issue before disclosing it to other parties (if after waiting a reasonable amount of time, we are clearly unable or unwilling to do anything about it, please do hold us accountable!) -* Please test against a local instance of the software, and refrain from running any Denial of Service or automated testing tools against Framasoft's (and our partners') infrastructure +* Please test against a local instance of the software, and refrain from running any Denial of Service or automated testing tools against the project managers (and their partners') infrastructure Note : Please report security bugs in third-party modules to the person or team maintaining the module. diff --git a/config/config.exs b/config/config.exs index 03c613a7c..6e89ba0a6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -36,6 +36,7 @@ config :mobilizon, :instance, unconfirmed_user_grace_period_hours: 48, activity_expire_days: 365, activity_keep_number: 100, + duration_of_long_event: 30, enable_instance_feeds: true, email_from: "noreply@localhost", email_reply_to: "noreply@localhost" @@ -205,6 +206,8 @@ config :codepagex, :encodings, [ :"VENDORS/MICSFT/WINDOWS/CP1252" ] +config :gettext, :default_locale, "en" + config :mobilizon, Mobilizon.Web.Gettext, split_module_by: [:locale, :domain] config :ex_cldr, diff --git a/config/dev.exs b/config/dev.exs index 429a84391..a12c110f0 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -5,9 +5,9 @@ config :mobilizon, Mobilizon.Web.Endpoint, port: String.to_integer(System.get_env("MOBILIZON_INSTANCE_HOST_PORT", "4000")) ], url: [ - host: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.local"), - port: String.to_integer(System.get_env("MOBILIZON_INSTANCE_HOST_PORT", "80")), - scheme: "http" + host: System.get_env("MOBILIZON_INSTANCE_HOST", "localhost"), + port: String.to_integer(System.get_env("MOBILIZON_INSTANCE_HOST_PORT", "4000")), + scheme: System.get_env("MOBILIZON_INSTANCE_SCHEME", "http") ], secret_key_base: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY_BASE", "changethis"), debug_errors: true, diff --git a/config/test.exs b/config/test.exs index 2e66b1a69..01dc74134 100644 --- a/config/test.exs +++ b/config/test.exs @@ -2,7 +2,8 @@ import Config config :mobilizon, :instance, name: "Test instance", - registrations_open: true + registrations_open: true, + duration_of_long_event: 0 # We don't run a server during test. If one is required, # you can enable the server option below. diff --git a/docker/multiarch/Dockerfile b/docker/multiarch/Dockerfile index 391507d76..77eee5027 100644 --- a/docker/multiarch/Dockerfile +++ b/docker/multiarch/Dockerfile @@ -1,4 +1,4 @@ -ARG IMAGE="elixir:1.15" +ARG IMAGE="elixir:1.16" FROM ${IMAGE} as build SHELL ["/bin/bash", "-c"] diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index d79986bb3..6091b2446 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -9,7 +9,7 @@ COPY . . RUN npm install && npm run build # Then, build the application binary -FROM elixir:1.15-alpine AS builder +FROM elixir:1.16-alpine AS builder # Fix qemu segfault on arm64 # See https://github.com/plausible/analytics/pull/2879 and https://github.com/erlang/otp/pull/6340 diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile index 7cccbada3..7c0d5b1d2 100644 --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -1,11 +1,10 @@ FROM elixir:latest -LABEL maintainer="Thomas Citharel " -ENV REFRESHED_AT=2023-11-20 +ENV REFRESHED_AT=2024-02-29 RUN apt-get update -yq && apt-get install -yq ca-certificates 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 mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -yq RUN npm install -g wait-on RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN mix local.hex --force && mix local.rebar --force -RUN pip3 --no-cache-dir install -Iv weasyprint pyexcel_ods3 +RUN pip --no-cache-dir install --break-system-packages weasyprint 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/ diff --git a/lib/graphql/api/events.ex b/lib/graphql/api/events.ex index a67787661..78bf77364 100644 --- a/lib/graphql/api/events.ex +++ b/lib/graphql/api/events.ex @@ -57,15 +57,25 @@ defmodule Mobilizon.GraphQL.API.Events do defp process_picture(%{media_id: _picture_id} = args, _), do: args defp process_picture(%{media: media}, %Actor{id: actor_id}) do - with uploaded when is_map(uploaded) <- - media - |> Map.get(:file) - |> Utils.make_media_data(description: Map.get(media, :name)) do + # case url + if Map.has_key?(media, :url) do %{ - file: Map.take(uploaded, [:url, :name, :content_type, :size]), - metadata: Map.take(uploaded, [:width, :height, :blurhash]), + file: %{"url" => media.url, "name" => media.name}, actor_id: actor_id } + + # case upload + else + with uploaded when is_map(uploaded) <- + media + |> Map.get(:file) + |> Utils.make_media_data(description: Map.get(media, :name)) do + %{ + file: Map.take(uploaded, [:url, :name, :content_type, :size]), + metadata: Map.take(uploaded, [:width, :height, :blurhash]), + actor_id: actor_id + } + end end end diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index fd0f81f5a..76bb1a362 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -5,11 +5,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do import Mobilizon.Users.Guards - alias Mobilizon.{Actors, Admin, Config, Events, Instances, Users} + alias Mobilizon.{Actors, Admin, Config, Events, Instances, Media, Users} alias Mobilizon.Actors.{Actor, Follower} - alias Mobilizon.Admin.{ActionLog, Setting} + alias Mobilizon.Admin.{ActionLog, Setting, SettingMedia} alias Mobilizon.Cldr.Language - alias Mobilizon.Config alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.{Actions, Relay} @@ -20,6 +19,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do alias Mobilizon.Storage.Page alias Mobilizon.Users.User alias Mobilizon.Web.Email + + alias Mobilizon.GraphQL.Resolvers.Media, as: MediaResolver + import Mobilizon.Web.Gettext require Logger @@ -268,8 +270,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do with {:ok, res} <- Admin.save_settings("instance", args), res <- res - |> Enum.map(fn {key, %Setting{value: value}} -> - {key, Admin.get_setting_value(value)} + |> Enum.map(fn {key, val} -> + case val do + %Setting{value: value} -> {key, Admin.get_setting_value(value)} + %SettingMedia{media: media} -> {key, media} + end end) |> Enum.into(%{}), :ok <- eventually_update_instance_actor(res) do @@ -284,6 +289,38 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do dgettext("errors", "You need to be logged-in and an administrator to save admin settings")} end + @spec get_media_setting(any(), any(), Absinthe.Resolution.t()) :: + {:ok, Media.t()} | {:error, String.t()} + def get_media_setting(_parent, %{group: group, name: name}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + {:ok, MediaResolver.transform_media(Admin.get_admin_setting_media(group, name, nil))} + end + + def get_media_setting(_parent, _args, _resolution) do + {:error, + dgettext("errors", "You need to be logged-in and an administrator to access admin settings")} + end + + @spec get_instance_logo(any(), any(), Absinthe.Resolution.t()) :: + {:ok, Media.t() | nil} | {:error, String.t()} + def get_instance_logo(parent, _args, resolution) do + get_media_setting(parent, %{group: "instance", name: "instance_logo"}, resolution) + end + + @spec get_instance_favicon(any(), any(), Absinthe.Resolution.t()) :: + {:ok, Media.t() | nil} | {:error, String.t()} + def get_instance_favicon(parent, _args, resolution) do + get_media_setting(parent, %{group: "instance", name: "instance_favicon"}, resolution) + end + + @spec get_default_picture(any(), any(), Absinthe.Resolution.t()) :: + {:ok, Media.t() | nil} | {:error, String.t()} + def get_default_picture(parent, _args, resolution) do + get_media_setting(parent, %{group: "instance", name: "default_picture"}, resolution) + end + @spec update_user(any, map(), Absinthe.Resolution.t()) :: {:error, :invalid_argument | :user_not_found | binary | Ecto.Changeset.t()} | {:ok, Mobilizon.Users.User.t()} diff --git a/lib/graphql/resolvers/config.ex b/lib/graphql/resolvers/config.ex index 8e5a316e4..10f90849d 100644 --- a/lib/graphql/resolvers/config.ex +++ b/lib/graphql/resolvers/config.ex @@ -5,8 +5,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do alias Mobilizon.Config alias Mobilizon.Events.Categories + alias Mobilizon.Medias.Media alias Mobilizon.Service.{AntiSpam, FrontEndAnalytics} + alias Mobilizon.GraphQL.Resolvers.Media, as: MediaResolver + @doc """ Gets config. """ @@ -31,6 +34,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do {:ok, data} end + @spec instance_logo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Media.t()} + def instance_logo(_parent, _params, _resolution) do + {:ok, MediaResolver.transform_media(Config.instance_logo())} + end + + @spec default_picture(any(), map(), Absinthe.Resolution.t()) :: {:ok, Media.t()} + def default_picture(_parent, _params, _resolution) do + {:ok, MediaResolver.transform_media(Config.default_picture())} + end + @spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} def terms(_parent, %{locale: locale}, _resolution) do type = Config.instance_terms_type() @@ -94,10 +107,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do registrations_allowlist: Config.instance_registrations_allowlist?(), contact: Config.contact(), demo_mode: Config.instance_demo_mode?(), + long_events: Config.instance_long_events?(), description: Config.instance_description(), long_description: Config.instance_long_description(), slogan: Config.instance_slogan(), languages: Config.instance_languages(), + instance_logo: Config.instance_logo(), + primary_color: Config.primary_color(), + secondary_color: Config.secondary_color(), + default_picture: Config.default_picture(), anonymous: %{ participation: %{ allowed: Config.anonymous_participation?(), diff --git a/lib/graphql/resolvers/media.ex b/lib/graphql/resolvers/media.ex index 579403799..7647657df 100644 --- a/lib/graphql/resolvers/media.ex +++ b/lib/graphql/resolvers/media.ex @@ -18,6 +18,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do do_fetch_media(media_id) end + def media(%{media_id: media_id} = _parent, _args, _resolution) do + do_fetch_media(media_id) + end + def media(%{picture: media} = _parent, _args, _resolution), do: {:ok, media} def media(_parent, %{id: media_id}, _resolution), do: do_fetch_media(media_id) def media(_parent, _args, _resolution), do: {:ok, nil} @@ -133,8 +137,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated} - @spec transform_media(Media.t()) :: map() - defp transform_media(%Media{id: id, file: file, metadata: metadata}) do + @spec transform_media(Media.t() | nil) :: map() | nil + def transform_media(nil), do: nil + + def transform_media(%Media{id: id, file: file, metadata: metadata}) do %{ name: file.name, url: file.url, diff --git a/lib/graphql/schema/admin.ex b/lib/graphql/schema/admin.ex index 59c6b810b..c5f2beb9a 100644 --- a/lib/graphql/schema/admin.ex +++ b/lib/graphql/schema/admin.ex @@ -124,6 +124,24 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do field(:instance_terms_type, :instance_terms_type, description: "The instance's terms type") field(:instance_terms_url, :string, description: "The instance's terms URL") + field(:instance_logo, :media, + description: "The instance's logo", + resolve: &Admin.get_instance_logo/3 + ) + + field(:instance_favicon, :media, + description: "The instance's favicon", + resolve: &Admin.get_instance_favicon/3 + ) + + field(:default_picture, :media, + description: "The default picture", + resolve: &Admin.get_default_picture/3 + ) + + field(:primary_color, :string, description: "The instance's primary color") + field(:secondary_color, :string, description: "The instance's secondary color") + field(:instance_privacy_policy, :string, description: "The instance's privacy policy body text" ) @@ -412,6 +430,25 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do arg(:instance_long_description, :string, description: "The instance's long description") arg(:instance_slogan, :string, description: "The instance's slogan") arg(:contact, :string, description: "The instance's contact details") + + arg(:instance_logo, :media_input, + description: + "The instance's logo, either as an object or directly the ID of an existing media" + ) + + arg(:instance_favicon, :media_input, + description: + "The instance's favicon, either as an object or directly the ID of an existing media" + ) + + arg(:default_picture, :media_input, + description: + "The default picture, either as an object or directly the ID of an existing media" + ) + + arg(:primary_color, :string, description: "The instance's primary color") + arg(:secondary_color, :string, description: "The instance's secondary color") + arg(:instance_terms, :string, description: "The instance's terms body text") arg(:instance_terms_type, :instance_terms_type, description: "The instance's terms type") arg(:instance_terms_url, :string, description: "The instance's terms URL") diff --git a/lib/graphql/schema/config.ex b/lib/graphql/schema/config.ex index 59579a6e3..b4e68180a 100644 --- a/lib/graphql/schema/config.ex +++ b/lib/graphql/schema/config.ex @@ -31,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do ) field(:demo_mode, :boolean, description: "Whether the demo mode is enabled") + field(:long_events, :boolean, description: "Whether the long events mode is enabled") field(:country_code, :string, description: "The country code from the IP") field(:location, :lonlat, description: "The IP's location") field(:geocoding, :geocoding, description: "The instance's geocoding settings") @@ -59,6 +60,17 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do resolve(&Config.terms/3) end + field(:instance_logo, :media, description: "The instance's logo") do + resolve(&Config.instance_logo/3) + end + + field(:default_picture, :media, description: "The default picture") do + resolve(&Config.default_picture/3) + end + + field(:primary_color, :string, description: "The instance's primary color") + field(:secondary_color, :string, description: "The instance's secondary color") + field(:privacy, :privacy, description: "The instance's privacy policy") do arg(:locale, :string, default_value: "en", diff --git a/lib/graphql/schema/event.ex b/lib/graphql/schema/event.ex index 9c6e04d5a..e2961794d 100644 --- a/lib/graphql/schema/event.ex +++ b/lib/graphql/schema/event.ex @@ -263,6 +263,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do description: "Whether or not to show the participation price" ) + field(:hide_number_of_participants, :boolean, + description: "Whether or not the number of participants is hidden" + ) + field(:show_start_time, :boolean, description: "Show event start time") field(:show_end_time, :boolean, description: "Show event end time") @@ -316,6 +320,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do description: "Whether or not to show the participation price" ) + field(:hide_number_of_participants, :boolean, + description: "Whether or not the number of participants is hidden" + ) + field(:show_start_time, :boolean, description: "Show event start time") field(:show_end_time, :boolean, description: "Show event end time") diff --git a/lib/graphql/schema/media.ex b/lib/graphql/schema/media.ex index 7e77b9fcb..8514ee406 100644 --- a/lib/graphql/schema/media.ex +++ b/lib/graphql/schema/media.ex @@ -52,8 +52,9 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do input_object :media_input_object do field(:name, non_null(:string), description: "The media's name") field(:alt, :string, description: "The media's alternative text") - field(:file, non_null(:upload), description: "The media file") + field(:file, :upload, description: "The media file") field(:actor_id, :id, description: "The media owner") + field(:url, :string, description: "The media URL") end object :media_queries do diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index 626a693c0..f518c13dc 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -273,6 +273,8 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do description: "Radius around the location to search in" ) + arg(:longevents, :boolean, description: "if mention filter in or out long events") + arg(:bbox, :string, description: "The bbox to search events into") arg(:zoom, :integer, description: "The zoom level for searching events") diff --git a/lib/mobilizon/admin/admin.ex b/lib/mobilizon/admin/admin.ex index 9a93b02f1..16ae49aa0 100644 --- a/lib/mobilizon/admin/admin.ex +++ b/lib/mobilizon/admin/admin.ex @@ -9,7 +9,8 @@ defmodule Mobilizon.Admin do alias Mobilizon.Actors.Actor alias Mobilizon.{Admin, Users} alias Mobilizon.Admin.ActionLog - alias Mobilizon.Admin.Setting + alias Mobilizon.Admin.{Setting, SettingMedia} + alias Mobilizon.Medias.Media alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Users.User @@ -78,9 +79,47 @@ defmodule Mobilizon.Admin do defp stringify_struct(struct), do: struct - @spec get_all_admin_settings :: list(Setting.t()) + @spec get_all_admin_settings :: map() def get_all_admin_settings do - Repo.all(Setting) + medias = + SettingMedia + |> Repo.all() + |> Repo.preload(:media) + |> Enum.map(fn %SettingMedia{group: group, name: name, media: media} -> + {group, name, media} + end) + + values = + Setting + |> Repo.all() + |> Enum.map(fn %Setting{group: group, name: name, value: value} -> + {group, name, get_setting_value(value)} + end) + + all_settings = Enum.concat(values, medias) + + Enum.reduce( + all_settings, + %{}, + # For each {group,name,value} + fn {group, name, value}, acc -> + # We update the %{group: map} in the accumulator + {_, new_acc} = + Map.get_and_update( + acc, + group, + # We put the %{name: value} into the %{group: map} + fn group_map -> + { + group_map, + Map.put(group_map || %{}, name, value) + } + end + ) + + new_acc + end + ) end @spec get_admin_setting_value(String.t(), String.t(), String.t() | nil) :: @@ -119,21 +158,40 @@ defmodule Mobilizon.Admin do end end + @spec get_admin_setting_media(String.t(), String.t(), String.t() | nil) :: + {:ok, Media.t()} | {:error, :not_found} | nil + def get_admin_setting_media(group, name, fallback \\ nil) + when is_binary(group) and is_binary(name) do + case SettingMedia + |> where(group: ^group) + |> where(name: ^name) + |> preload(:media) + |> Repo.one() do + nil -> + fallback + + %SettingMedia{media: media} -> + media + + %SettingMedia{} -> + fallback + end + end + @spec save_settings(String.t(), map()) :: {:ok, any} | {:error, any} def save_settings(group, args) do + {medias, values} = Map.split(args, [:instance_logo, :instance_favicon, :default_picture]) + Multi.new() - |> do_save_setting(group, args) + |> do_save_media_setting(group, medias) + |> do_save_value_setting(group, values) |> Repo.transaction() end - def clear_settings(group) do - Setting |> where([s], s.group == ^group) |> Repo.delete_all() - end + @spec do_save_value_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t() + defp do_save_value_setting(transaction, _group, args) when args == %{}, do: transaction - @spec do_save_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t() - defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction - - defp do_save_setting(transaction, group, args) do + defp do_save_value_setting(transaction, group, args) do key = hd(Map.keys(args)) {val, rest} = Map.pop(args, key) @@ -150,7 +208,40 @@ defmodule Mobilizon.Admin do conflict_target: [:group, :name] ) - do_save_setting(transaction, group, rest) + do_save_value_setting(transaction, group, rest) + end + + @spec do_save_media_setting(Ecto.Multi.t(), String.t(), map()) :: Ecto.Multi.t() + defp do_save_media_setting(transaction, _group, args) when args == %{}, do: transaction + + defp do_save_media_setting(transaction, group, args) do + key = hd(Map.keys(args)) + {val, rest} = Map.pop(args, key) + + transaction = + case val do + val -> + Multi.insert( + transaction, + key, + SettingMedia.changeset(%SettingMedia{}, %{ + group: group, + name: Atom.to_string(key), + media: val + }), + on_conflict: :replace_all, + conflict_target: [:group, :name] + ) + end + + do_save_media_setting(transaction, group, rest) + end + + def clear_settings(group) do + Multi.new() + |> Multi.delete_all(:settings, Setting |> where([s], s.group == ^group)) + |> Multi.delete_all(:settings_medias, SettingMedia |> where([s], s.group == ^group)) + |> Repo.transaction() end @spec convert_to_string(any()) :: String.t() diff --git a/lib/mobilizon/admin/setting.ex b/lib/mobilizon/admin/setting.ex index a505358ec..cf363a61c 100644 --- a/lib/mobilizon/admin/setting.ex +++ b/lib/mobilizon/admin/setting.ex @@ -4,6 +4,7 @@ defmodule Mobilizon.Admin.Setting do """ use Ecto.Schema import Ecto.Changeset + alias Ecto.Changeset @required_attrs [:group, :name] @optional_attrs [:value] @@ -32,3 +33,93 @@ defmodule Mobilizon.Admin.Setting do |> unique_constraint(:group, name: :admin_settings_group_name_index) end end + +defmodule Mobilizon.Admin.SettingMedia do + @moduledoc """ + A Key-Value settings table for media settings + """ + use Ecto.Schema + import Ecto.Changeset + alias Ecto.Changeset + alias Mobilizon.Federation.ActivityPub.Relay + alias Mobilizon.Medias + alias Mobilizon.Medias.Media + alias Mobilizon.Storage.Repo + + @required_attrs [:group, :name] + + @type t :: %{ + group: String.t(), + name: String.t(), + media: Media.t() + } + + schema "admin_settings_medias" do + field(:group, :string) + field(:name, :string) + belongs_to(:media, Media, on_replace: :delete) + + timestamps() + end + + @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() + def changeset(setting_media, attrs) do + setting_media + |> Repo.preload(:media) + |> cast(attrs, @required_attrs) + |> put_media(attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:group, name: :admin_settings_medias_group_name_index) + end + + # # In case the provided media is an existing one + @spec put_media(Changeset.t(), map) :: Changeset.t() + defp put_media(%Changeset{} = changeset, %{media: %{media_id: id}}) do + %Media{} = media = Medias.get_media!(id) + put_assoc(changeset, :media, media) + end + + # In case it's a new media + defp put_media(%Changeset{} = changeset, %{media: %{media: media}}) do + {:ok, media} = upload_media(media) + put_assoc(changeset, :media, media) + end + + # In case there is no media + defp put_media(%Changeset{} = changeset, _media) do + put_assoc(changeset, :media, nil) + end + + import Mobilizon.Web.Gettext + @spec upload_media(map) :: {:ok, Media.t()} | {:error, any} + defp upload_media(%{file: %Plug.Upload{} = file} = args) do + with {:ok, + %{ + name: _name, + url: url, + content_type: content_type, + size: size + } = uploaded} <- + Mobilizon.Web.Upload.store(file), + args <- + args + |> Map.put(:url, url) + |> Map.put(:size, size) + |> Map.put(:content_type, content_type), + {:ok, media = %Media{}} <- + Medias.create_media(%{ + file: args, + actor_id: Map.get(args, :actor_id, Relay.get_actor().id), + metadata: Map.take(uploaded, [:width, :height, :blurhash]) + }) do + {:ok, media} + else + {:error, :mime_type_not_allowed} -> + {:error, dgettext("errors", "File doesn't have an allowed MIME type.")} + + error -> + {:error, error} + end + end +end diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex index 92a29544e..dc52baa1c 100644 --- a/lib/mobilizon/config.ex +++ b/lib/mobilizon/config.ex @@ -4,7 +4,8 @@ defmodule Mobilizon.Config do """ alias Mobilizon.Actors - alias Mobilizon.Admin.Setting + alias Mobilizon.Admin + alias Mobilizon.Medias.Media alias Mobilizon.Service.GitStatus require Logger import Mobilizon.Service.Export.Participants.Common, only: [enabled_formats: 0] @@ -29,56 +30,18 @@ defmodule Mobilizon.Config do @spec instance_config :: mobilizon_config def instance_config, do: Application.get_env(:mobilizon, :instance) - @spec db_instance_config :: list(Setting.t()) - def db_instance_config, do: Mobilizon.Admin.get_all_admin_settings() - @spec config_cache :: map() def config_cache do - case Cachex.fetch(:config, :all_db_config, fn _key -> - value = - Enum.reduce( - Mobilizon.Admin.get_all_admin_settings(), - %{}, - &arrange_values/2 - ) - - {:commit, value} - end) do + case Cachex.fetch( + :config, + :all_db_config, + fn _key -> {:commit, Admin.get_all_admin_settings()} end + ) do {status, value} when status in [:ok, :commit] -> value _err -> %{} end end - @spec arrange_values(Setting.t(), map()) :: map() - defp arrange_values(setting, acc) do - {_, new_data} = - Map.get_and_update(acc, setting.group, fn current_value -> - new_value = current_value || %{} - - {current_value, Map.put(new_value, setting.name, process_value(setting.value))} - end) - - new_data - end - - @spec process_value(String.t() | nil) :: any() - defp process_value(nil), do: nil - defp process_value(""), do: nil - - defp process_value(value) do - case Jason.decode(value) do - {:ok, val} -> - val - - {:error, _} -> - case value do - "true" -> true - "false" -> false - value -> value - end - end - end - @spec config_cached_value(String.t(), String.t(), String.t()) :: any() def config_cached_value(group, name, fallback \\ nil) do config_cache() @@ -115,10 +78,23 @@ defmodule Mobilizon.Config do @spec instance_slogan :: String.t() | nil def instance_slogan, do: config_cached_value("instance", "instance_slogan") + @spec instance_logo :: Media.t() | nil + def instance_logo, do: config_cached_value("instance", "instance_logo") + + @spec instance_favicon :: Media.t() | nil + def instance_favicon, do: config_cached_value("instance", "instance_favicon") + + @spec default_picture :: Media.t() | nil + def default_picture, do: config_cached_value("instance", "default_picture") + + @spec primary_color :: Media.t() | nil + def primary_color, do: config_cached_value("instance", "primary_color") + + @spec secondary_color :: Media.t() | nil + def secondary_color, do: config_cached_value("instance", "secondary_color") + @spec contact :: String.t() | nil - def contact do - config_cached_value("instance", "contact") - end + def contact, do: config_cached_value("instance", "contact") @spec instance_terms(String.t()) :: String.t() def instance_terms(locale \\ "en") do @@ -202,6 +178,9 @@ defmodule Mobilizon.Config do @spec instance_demo_mode? :: boolean def instance_demo_mode?, do: to_boolean(instance_config()[:demo]) + @spec instance_long_events? :: boolean + def instance_long_events?, do: instance_config()[:duration_of_long_event] > 0 + @spec instance_repository :: String.t() def instance_repository, do: instance_config()[:repository] @@ -470,6 +449,9 @@ defmodule Mobilizon.Config do instance_slogan: instance_slogan(), registrations_open: instance_registrations_open?(), contact: contact(), + primary_color: primary_color(), + secondary_color: secondary_color(), + instance_logo: instance_logo(), instance_terms: instance_terms(), instance_terms_type: instance_terms_type(), instance_terms_url: instance_terms_url(), diff --git a/lib/mobilizon/events/event_options.ex b/lib/mobilizon/events/event_options.ex index 3a2780b9b..894d33106 100644 --- a/lib/mobilizon/events/event_options.ex +++ b/lib/mobilizon/events/event_options.ex @@ -25,6 +25,7 @@ defmodule Mobilizon.Events.EventOptions do show_participation_price: boolean, offers: [EventOffer.t()], participation_condition: [EventParticipationCondition.t()], + hide_number_of_participants: boolean, show_start_time: boolean, show_end_time: boolean, timezone: String.t() | nil, @@ -41,6 +42,7 @@ defmodule Mobilizon.Events.EventOptions do :program, :comment_moderation, :show_participation_price, + :hide_number_of_participants, :show_start_time, :show_end_time, :timezone, @@ -59,6 +61,7 @@ defmodule Mobilizon.Events.EventOptions do field(:program, :string) field(:comment_moderation, CommentModeration) field(:show_participation_price, :boolean) + field(:hide_number_of_participants, :boolean, default: false) field(:show_start_time, :boolean, default: true) field(:show_end_time, :boolean, default: true) field(:timezone, :string) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index bc97ab50d..1009a0197 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -16,6 +16,7 @@ defmodule Mobilizon.Events do alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Addresses.Address + alias Mobilizon.Config alias Mobilizon.Events.{ Event, @@ -571,6 +572,7 @@ defmodule Mobilizon.Events do |> events_for_search_query() |> events_for_begins_on(Map.get(args, :begins_on, DateTime.utc_now())) |> events_for_ends_on(Map.get(args, :ends_on)) + |> events_for_longevents(args) |> events_for_category(args) |> events_for_categories(args) |> events_for_languages(args) @@ -1377,6 +1379,38 @@ defmodule Mobilizon.Events do end end + @spec events_for_longevents(Ecto.Queryable.t(), map()) :: Ecto.Query.t() + defp events_for_longevents(query, args) do + duration = Config.get([:instance, :duration_of_long_event], 0) + + if duration <= 0 do + query + else + longevents = Map.get(args, :longevents) + + case longevents do + nil -> + query + + true -> + where( + query, + [q], + not is_nil(q.ends_on) and + q.ends_on > fragment("? + '1 days'::interval * ?", q.begins_on, ^duration) + ) + + false -> + where( + query, + [q], + is_nil(q.ends_on) or + q.ends_on <= fragment("? + '1 days'::interval * ?", q.begins_on, ^duration) + ) + end + end + end + @spec events_for_category(Ecto.Queryable.t(), map()) :: Ecto.Query.t() defp events_for_category(query, %{category: category}) when is_valid_string(category) do where(query, [q], q.category == ^category) diff --git a/lib/mobilizon/medias/medias.ex b/lib/mobilizon/medias/medias.ex index 29cccd780..06c8ce500 100644 --- a/lib/mobilizon/medias/medias.ex +++ b/lib/mobilizon/medias/medias.ex @@ -185,7 +185,8 @@ defmodule Mobilizon.Medias do [from: "events_medias", param: "media_id"], [from: "posts", param: "picture_id"], [from: "posts_medias", param: "media_id"], - [from: "comments_medias", param: "media_id"] + [from: "comments_medias", param: "media_id"], + [from: "admin_settings_medias", param: "media_id"] ] |> Enum.map_join(" UNION ", fn [from: from, param: param] -> "SELECT 1 FROM #{from} WHERE #{from}.#{param} = m0.id" diff --git a/lib/service/geospatial/hat.ex b/lib/service/geospatial/hat.ex new file mode 100644 index 000000000..d743ee34b --- /dev/null +++ b/lib/service/geospatial/hat.ex @@ -0,0 +1,44 @@ +defmodule Mobilizon.Service.Geospatial.Hat do + @moduledoc """ + Hat backend. + """ + + alias Mobilizon.Addresses.Address + alias Mobilizon.Service.Geospatial.Addok + alias Mobilizon.Service.Geospatial.Nominatim + alias Mobilizon.Service.Geospatial.Provider + import Mobilizon.Service.Geospatial.Provider, only: [endpoint: 1] + require Logger + + @behaviour Provider + + @impl Provider + @doc """ + Hat implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`. + """ + @spec geocode(String.t(), keyword()) :: list(Address.t()) + def geocode(lon, lat, options \\ []) do + tasks = [ + Task.async(fn -> Addok.geocode(lon, lat, options) end), + Task.async(fn -> Nominatim.geocode(lon, lat, options) end) + ] + + [addrlist1, addrlist2] = Task.await_many(tasks, 12_000) + addrlist2 ++ addrlist1 + end + + @impl Provider + @doc """ + Hat implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`. + """ + @spec search(String.t(), keyword()) :: list(Address.t()) + def search(q, options \\ []) do + tasks = [ + Task.async(fn -> Addok.search(q, options) end), + Task.async(fn -> Nominatim.search(q, options) end) + ] + + [addrlist1, addrlist2] = Task.await_many(tasks, 12_000) + addrlist2 ++ addrlist1 + end +end diff --git a/lib/web/controllers/manifest_controller.ex b/lib/web/controllers/manifest_controller.ex new file mode 100644 index 000000000..c26f841f9 --- /dev/null +++ b/lib/web/controllers/manifest_controller.ex @@ -0,0 +1,58 @@ +defmodule Mobilizon.Web.ManifestController do + use Mobilizon.Web, :controller + + alias Mobilizon.Config + alias Mobilizon.Medias.Media + + @spec manifest(Plug.Conn.t(), any) :: Plug.Conn.t() + def manifest(conn, _params) do + favicons = + case Config.instance_favicon() do + %Media{file: %{url: url}, metadata: metadata} -> + [ + Map.merge( + %{ + src: url + }, + case metadata do + %{width: width} -> %{sizes: "#{width}x#{width}"} + _ -> %{} + end + ) + ] + + _ -> + [ + %{ + src: "./img/icons/android-chrome-512x512.png", + sizes: "512x512", + type: "image/png" + }, + %{ + src: "./img/icons/android-chrome-192x192.png", + sizes: "192x192", + type: "image/png" + } + ] + end + + json(conn, %{ + name: Config.instance_name(), + start_url: "/", + scope: "/", + display: "standalone", + background_color: "#ffffff", + theme_color: "#ffd599", + orientation: "portrait-primary", + icons: favicons + }) + end + + @spec favicon(Plug.Conn.t(), any) :: Plug.Conn.t() + def favicon(conn, _params) do + case Config.instance_favicon() do + %Media{file: %{url: url}} -> redirect(conn, external: url) + _ -> redirect(conn, to: "/img/icons/favicon.ico") + end + end +end diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex index 486791eef..e24b49a58 100644 --- a/lib/web/controllers/page_controller.ex +++ b/lib/web/controllers/page_controller.ex @@ -18,6 +18,8 @@ defmodule Mobilizon.Web.PageController do defdelegate my_events(conn, params), to: PageController, as: :index @spec create_event(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate create_event(conn, params), to: PageController, as: :index + @spec calendar(Plug.Conn.t(), any) :: Plug.Conn.t() + defdelegate calendar(conn, params), to: PageController, as: :index @spec list_events(Plug.Conn.t(), any) :: Plug.Conn.t() defdelegate list_events(conn, params), to: PageController, as: :index @spec edit_event(Plug.Conn.t(), any) :: Plug.Conn.t() diff --git a/lib/web/mobilizon_web.ex b/lib/web/mobilizon_web.ex index a1f97a6e3..9848b880b 100644 --- a/lib/web/mobilizon_web.ex +++ b/lib/web/mobilizon_web.ex @@ -18,8 +18,7 @@ defmodule Mobilizon.Web do """ def static_paths, - do: - ~w(index.html manifest.json manifest.webmanifest service-worker.js css fonts img js favicon.ico robots.txt assets) + do: ~w(index.html service-worker.js css fonts img js robots.txt assets) def controller do quote do diff --git a/lib/web/plugs/http_security_plug.ex b/lib/web/plugs/http_security_plug.ex index 862803b49..eeb2f3702 100644 --- a/lib/web/plugs/http_security_plug.ex +++ b/lib/web/plugs/http_security_plug.ex @@ -77,7 +77,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do # unsafe-eval is because of JS issues with regenerator-runtime @script_src "script-src 'self' 'unsafe-eval' " @style_src "style-src 'self' " - @font_src "font-src 'self' " + @font_src "font-src 'self' data: " @spec csp_string(Keyword.t()) :: String.t() defp csp_string(options) do @@ -117,6 +117,8 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do style_src = [style_src] ++ [get_csp_config(:style_src, options)] + style_src = [style_src] ++ ["'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"] + font_src = [@font_src] ++ [get_csp_config(:font_src, options)] frame_src = build_csp_field(:frame_src, options) diff --git a/lib/web/router.ex b/lib/web/router.ex index 131c44125..a960fba36 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -113,6 +113,12 @@ defmodule Mobilizon.Web.Router do get("/nodeinfo/:version", NodeInfoController, :nodeinfo) end + scope "/", Mobilizon.Web do + get("/manifest.webmanifest", ManifestController, :manifest) + get("/manifest.json", ManifestController, :manifest) + get("/favicon.ico", ManifestController, :favicon) + end + scope "/", Mobilizon.Web do pipe_through(:activity_pub_and_html) pipe_through(:activity_pub_signature) @@ -120,6 +126,7 @@ defmodule Mobilizon.Web.Router do get("/@:name", PageController, :actor) get("/events/me", PageController, :my_events) get("/events/create", PageController, :create_event) + get("/events/calendar", PageController, :calendar) get("/events/:uuid", PageController, :event) get("/comments/:uuid", PageController, :comment) get("/resource/:uuid", PageController, :resource) @@ -188,6 +195,7 @@ defmodule Mobilizon.Web.Router do get("/events/create", PageController, :create_event) get("/events/list", PageController, :list_events) get("/events/me", PageController, :my_events) + get("/events/calendar", PageController, :calendar) get("/events/:uuid/edit", PageController, :edit_event) # This is a hack to ease link generation into emails diff --git a/lib/web/templates/api/terms.html.heex b/lib/web/templates/api/terms.html.heex index 8f7f8366f..dafe7b100 100644 --- a/lib/web/templates/api/terms.html.heex +++ b/lib/web/templates/api/terms.html.heex @@ -54,7 +54,7 @@

<%= pgettext( "terms", - "When we say “we”, “our”, or “us” in this document, we are referring to the owners, operators and administrators of this Mobilizon instance. The Mobilizon software is provided by the team of Mobilizon contributors, supported by Framasoft, a French not-for-profit organization advocating for Free/Libre Software. Unless explicitly stated, this Mobilizon instance is an independent service using Mobilizon's source code. You may find more information about this instance on the \"About this instance\" page." + "When we say “we”, “our”, or “us” in this document, we are referring to the owners, operators and administrators of this Mobilizon instance. The Mobilizon software is provided by the team of Mobilizon contributors. Unless explicitly stated, this Mobilizon instance is an independent service using Mobilizon's source code. You may find more information about this instance on the \"About this instance\" page." ) |> raw %>

diff --git a/lib/web/templates/page/index.html.heex b/lib/web/templates/page/index.html.heex index 620bb39fd..7cf7efa13 100644 --- a/lib/web/templates/page/index.html.heex +++ b/lib/web/templates/page/index.html.heex @@ -4,15 +4,16 @@ - + + <%= if root?(assigns) do %> @@ -24,6 +25,7 @@ <%= Vite.vite_client() %> <%= Vite.vite_snippet("src/main.ts") %> +