diff --git a/.codespellrc b/.codespellrc index 1c1da2564..251af7fcf 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,2 +1,2 @@ [codespell] -ignore-words-list = aNULL,brose,doub,Udo,re-use,re-used,registr,shema +ignore-words-list = aNULL,brose,doub,Udo,re-use,re-used,registr,shema,commet,Commet diff --git a/group_vars/matrix_servers b/group_vars/matrix_servers index 1b4bcea50..5a9956fdf 100755 --- a/group_vars/matrix_servers +++ b/group_vars/matrix_servers @@ -604,6 +604,13 @@ devture_systemd_service_manager_services_list_auto: | 'groups': ['matrix', 'clients', 'schildichat', 'client-schildichat'], }] if matrix_client_schildichat_enabled else []) + + ([{ + 'name': 'matrix-client-commet.service', + 'priority': 2000, + 'restart_necessary': (matrix_client_commet_restart_necessary | bool), + 'groups': ['matrix', 'clients', 'commet', 'client-commet'], + }] if matrix_client_commet_enabled else []) + + ([{ 'name': 'matrix-client-fluffychat.service', 'priority': 2000, @@ -4392,6 +4399,18 @@ matrix_client_element_container_additional_networks: "{{ [matrix_playbook_revers matrix_client_element_container_labels_traefik_enabled: "{{ matrix_playbook_reverse_proxy_type in ['playbook-managed-traefik', 'other-traefik-container'] }}" matrix_client_element_container_labels_traefik_docker_network: "{{ matrix_playbook_reverse_proxyable_services_additional_network }}" + +matrix_client_commet_container_http_host_bind_port: "{{ (matrix_playbook_service_host_bind_interface_prefix ~ '8766') if matrix_playbook_service_host_bind_interface_prefix else '' }}" + +matrix_client_commet_container_network: "{{ matrix_addons_container_network }}" + +matrix_client_commet_container_additional_networks: "{{ [matrix_playbook_reverse_proxyable_services_additional_network] if (matrix_client_commet_container_labels_traefik_enabled and matrix_playbook_reverse_proxyable_services_additional_network) else [] }}" + +matrix_client_commet_container_labels_traefik_enabled: "{{ matrix_playbook_reverse_proxy_type in ['playbook-managed-traefik', 'other-traefik-container'] }}" +matrix_client_commet_container_labels_traefik_docker_network: "{{ matrix_playbook_reverse_proxyable_services_additional_network }}" +matrix_client_commet_container_labels_traefik_entrypoints: "{{ traefik_entrypoint_primary }}" +matrix_client_commet_container_labels_traefik_tls_certResolver: "{{ traefik_certResolver_primary }}" + matrix_client_element_container_labels_traefik_entrypoints: "{{ traefik_entrypoint_primary }}" matrix_client_element_container_labels_traefik_tls_certResolver: "{{ traefik_certResolver_primary }}" diff --git a/roles/custom/matrix-client-commet/defaults/main.yml b/roles/custom/matrix-client-commet/defaults/main.yml new file mode 100644 index 000000000..17ba27b7e --- /dev/null +++ b/roles/custom/matrix-client-commet/defaults/main.yml @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# Project source code URL: https://github.com/commetchat/commet + +matrix_client_commet_enabled: true + +# The git branch, tag, or SHA to build from +matrix_client_commet_version: "main" + +# The hostname at which Commet is served (e.g. commet.example.com) +matrix_client_commet_hostname: "" + +# The path at which Commet is exposed. +# This value must either be `/` or not end with a slash (e.g. `/commet`). +matrix_client_commet_path_prefix: / + +matrix_client_commet_base_path: "{{ matrix_base_data_path }}/client-commet" +matrix_client_commet_container_src_path: "{{ matrix_client_commet_base_path }}/container-src" +matrix_client_commet_config_path: "{{ matrix_client_commet_base_path }}/config" + +# Set to false to pull a pre-built image from a registry instead of building on the server. +matrix_client_commet_container_image_self_build: true + +# Self-build settings (used when matrix_client_commet_container_image_self_build: true) +matrix_client_commet_container_image_self_build_repo: "https://github.com/commetchat/commet.git" +# Populated automatically after git clone in setup_install.yml +matrix_client_commet_container_image_self_build_git_hash: "" +matrix_client_commet_container_image_self_build_version_tag: "{{ matrix_client_commet_version }}" +matrix_client_commet_container_image: "localhost/matrix-client-commet:{{ matrix_client_commet_version }}" + +# The in-container port nginx listens on +matrix_client_commet_container_port: 8080 + +# Optionally expose the container port on the host. +# Takes an ":" or "" value (e.g. "127.0.0.1:8765"), or empty string to not expose. +matrix_client_commet_container_http_host_bind_port: "" + +# The base container network +matrix_client_commet_container_network: "" + +# Additional container networks the container is connected to. +# The role does not create these networks, so make sure they already exist. +matrix_client_commet_container_additional_networks: [] + +# Runtime configuration — mounted into the container, not baked into the image +matrix_client_commet_default_homeserver: "matrix.org" + +# --------------------------------------------------------------------------- +# Traefik labels +# --------------------------------------------------------------------------- +matrix_client_commet_container_labels_traefik_enabled: true +matrix_client_commet_container_labels_traefik_docker_network: "{{ matrix_client_commet_container_network }}" +matrix_client_commet_container_labels_traefik_hostname: "{{ matrix_client_commet_hostname }}" +# The path prefix must either be `/` or not end with a slash (e.g. `/commet`). +matrix_client_commet_container_labels_traefik_path_prefix: "{{ matrix_client_commet_path_prefix }}" +matrix_client_commet_container_labels_traefik_rule: "Host(`{{ matrix_client_commet_container_labels_traefik_hostname }}`){% if matrix_client_commet_container_labels_traefik_path_prefix != '/' %} && PathPrefix(`{{ matrix_client_commet_container_labels_traefik_path_prefix }}`){% endif %}" +matrix_client_commet_container_labels_traefik_priority: 0 +matrix_client_commet_container_labels_traefik_entrypoints: web-secure +matrix_client_commet_container_labels_traefik_tls: "{{ matrix_client_commet_container_labels_traefik_entrypoints != 'web' }}" +matrix_client_commet_container_labels_traefik_tls_certResolver: default # noqa var-naming + +# Controls whether a compression middleware will be injected into the middlewares list. +matrix_client_commet_container_labels_traefik_compression_middleware_enabled: false +matrix_client_commet_container_labels_traefik_compression_middleware_name: "" + +# Additional response headers (auto-built from security header variables below) +matrix_client_commet_container_labels_traefik_additional_response_headers: "{{ matrix_client_commet_container_labels_traefik_additional_response_headers_auto | combine(matrix_client_commet_container_labels_traefik_additional_response_headers_custom) }}" +matrix_client_commet_container_labels_traefik_additional_response_headers_auto: | + {{ + {} + | combine({'X-XSS-Protection': matrix_client_commet_http_header_xss_protection} if matrix_client_commet_http_header_xss_protection else {}) + | combine({'X-Content-Type-Options': matrix_client_commet_http_header_content_type_options} if matrix_client_commet_http_header_content_type_options else {}) + | combine({'Content-Security-Policy': matrix_client_commet_http_header_content_security_policy} if matrix_client_commet_http_header_content_security_policy else {}) + | combine({'Strict-Transport-Security': matrix_client_commet_http_header_strict_transport_security} if matrix_client_commet_http_header_strict_transport_security and matrix_client_commet_container_labels_traefik_tls else {}) + }} +matrix_client_commet_container_labels_traefik_additional_response_headers_custom: {} + +# Additional container labels (multiline string) +matrix_client_commet_container_labels_additional_labels: "" + +# Extra arguments to pass to docker create +matrix_client_commet_container_extra_arguments: [] + +# --------------------------------------------------------------------------- +# HTTP security headers +# --------------------------------------------------------------------------- +matrix_client_commet_http_header_xss_protection: "1; mode=block" +matrix_client_commet_http_header_content_type_options: nosniff +matrix_client_commet_http_header_content_security_policy: "frame-ancestors 'self'" +matrix_client_commet_http_header_strict_transport_security: "max-age=31536000; includeSubDomains" + +# --------------------------------------------------------------------------- +# Systemd +# --------------------------------------------------------------------------- +matrix_client_commet_systemd_required_services_list: "{{ [devture_systemd_docker_base_docker_service_name] if devture_systemd_docker_base_docker_service_name else [] }}" + +# matrix_client_commet_restart_necessary is automatically set during installation +# to signal whether the service should be restarted after setup. +matrix_client_commet_restart_necessary: false diff --git a/roles/custom/matrix-client-commet/tasks/main.yml b/roles/custom/matrix-client-commet/tasks/main.yml new file mode 100644 index 000000000..c5a421018 --- /dev/null +++ b/roles/custom/matrix-client-commet/tasks/main.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- tags: + - setup-all + - setup-client-commet + - install-all + - install-client-commet + block: + - when: matrix_client_commet_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_install.yml" + +- tags: + - setup-all + - setup-client-commet + block: + - when: not matrix_client_commet_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/setup_uninstall.yml" + +- tags: + - self-check + block: + - when: matrix_client_commet_enabled | bool + ansible.builtin.debug: + msg: >- + Commet is running at + https://{{ matrix_client_commet_hostname }}{{ matrix_client_commet_path_prefix }} diff --git a/roles/custom/matrix-client-commet/tasks/setup_install.yml b/roles/custom/matrix-client-commet/tasks/setup_install.yml new file mode 100644 index 000000000..07b44e15d --- /dev/null +++ b/roles/custom/matrix-client-commet/tasks/setup_install.yml @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: 2025 Nikita Chernyi +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Ensure Commet paths exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0750" + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + with_items: + - "{{ matrix_client_commet_base_path }}" + - "{{ matrix_client_commet_config_path }}" + +- name: Ensure Commet container image is pulled + community.docker.docker_image: + name: "{{ matrix_client_commet_container_image }}" + source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}" + force_source: "{{ matrix_client_commet_container_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}" + force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_client_commet_container_image_force_pull }}" + when: "not matrix_client_commet_container_image_self_build | bool" + register: matrix_client_commet_image_pull_result + retries: "{{ devture_playbook_help_container_retries_count }}" + delay: "{{ devture_playbook_help_container_retries_delay }}" + until: matrix_client_commet_image_pull_result is not failed + +- when: "matrix_client_commet_container_image_self_build | bool" + block: + - name: Check Commet git repository metadata exists + ansible.builtin.stat: + path: "{{ matrix_client_commet_container_src_path }}/.git/config" + register: matrix_client_commet_git_config_file_stat + + - name: Remove Commet source directory if git remote is misconfigured + ansible.builtin.file: + path: "{{ matrix_client_commet_container_src_path }}" + state: absent + when: not matrix_client_commet_git_config_file_stat.stat.exists + become: true + + - name: Ensure Commet repository is present on self-build + ansible.builtin.git: + repo: "{{ matrix_client_commet_container_image_self_build_repo }}" + dest: "{{ matrix_client_commet_container_src_path }}" + version: "{{ matrix_client_commet_version }}" + force: "yes" + become: true + become_user: "{{ matrix_user_name }}" + register: matrix_client_commet_git_pull_results + + - name: Set git hash fact + ansible.builtin.set_fact: + matrix_client_commet_container_image_self_build_git_hash: "{{ matrix_client_commet_git_pull_results.after }}" + + - name: Ensure Commet container image is built + ansible.builtin.command: + cmd: |- + {{ devture_systemd_docker_base_host_command_docker }} buildx build + --tag={{ matrix_client_commet_container_image }} + --build-arg GIT_HASH={{ matrix_client_commet_container_image_self_build_git_hash }} + --build-arg VERSION_TAG={{ matrix_client_commet_container_image_self_build_version_tag }} + --build-arg BUILD_DATE={{ ansible_date_time.epoch }} + --file={{ matrix_client_commet_container_src_path }}/Dockerfile + {{ matrix_client_commet_container_src_path }} + changed_when: true + register: matrix_client_commet_image_build_result + +- name: Ensure Commet global_config.json is installed + ansible.builtin.template: + src: "{{ role_path }}/templates/global_config.json.j2" + dest: "{{ matrix_client_commet_config_path }}/global_config.json" + mode: "0644" + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + register: matrix_client_commet_config_result + +- name: Ensure Commet support files are installed + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ matrix_client_commet_base_path }}/{{ item.name }}" + mode: "0644" + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + with_items: + - {src: "{{ role_path }}/templates/labels.j2", name: "labels"} + - {src: "{{ role_path }}/templates/env.j2", name: "env"} + register: matrix_client_commet_support_files_result + +- name: Ensure Commet container network is created + community.general.docker_network: + enable_ipv6: "{{ devture_systemd_docker_base_ipv6_enabled }}" + name: "{{ matrix_client_commet_container_network }}" + driver: bridge + driver_options: "{{ devture_systemd_docker_base_container_networks_driver_options }}" + +- name: Ensure matrix-client-commet.service is installed + ansible.builtin.template: + src: "{{ role_path }}/templates/systemd/matrix-client-commet.service.j2" + dest: "{{ devture_systemd_docker_base_systemd_path }}/matrix-client-commet.service" + mode: "0644" + register: matrix_client_commet_systemd_service_result + +- name: Determine whether Commet needs a restart + ansible.builtin.set_fact: + matrix_client_commet_restart_necessary: >- + {{ + matrix_client_commet_config_result.changed | default(false) + or matrix_client_commet_support_files_result.changed | default(false) + or matrix_client_commet_systemd_service_result.changed | default(false) + or matrix_client_commet_image_build_result.changed | default(false) + or matrix_client_commet_image_pull_result.changed | default(false) + }} diff --git a/roles/custom/matrix-client-commet/tasks/setup_uninstall.yml b/roles/custom/matrix-client-commet/tasks/setup_uninstall.yml new file mode 100644 index 000000000..624aeea29 --- /dev/null +++ b/roles/custom/matrix-client-commet/tasks/setup_uninstall.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Check existence of matrix-client-commet.service + ansible.builtin.stat: + path: "{{ devture_systemd_docker_base_systemd_path }}/matrix-client-commet.service" + register: matrix_client_commet_service_stat + +- when: matrix_client_commet_service_stat.stat.exists | bool + block: + - name: Ensure matrix-client-commet is stopped + ansible.builtin.service: + name: matrix-client-commet + state: stopped + enabled: false + daemon_reload: true + + - name: Ensure matrix-client-commet.service doesn't exist + ansible.builtin.file: + path: "{{ devture_systemd_docker_base_systemd_path }}/matrix-client-commet.service" + state: absent + + - name: Ensure Commet path doesn't exist + ansible.builtin.file: + path: "{{ matrix_client_commet_base_path }}" + state: absent diff --git a/roles/custom/matrix-client-commet/templates/env.j2 b/roles/custom/matrix-client-commet/templates/env.j2 new file mode 100644 index 000000000..cca507825 --- /dev/null +++ b/roles/custom/matrix-client-commet/templates/env.j2 @@ -0,0 +1,12 @@ +{# +SPDX-FileCopyrightText: 2026 MDAD project contributors + +SPDX-License-Identifier: AGPL-3.0-or-later +#} + +{# +Environment variables for the matrix-client-commet container. +Add custom variables by appending to matrix_client_commet_environment_variables_extension. +#} + +{{ matrix_client_commet_environment_variables_extension | default('') }} diff --git a/roles/custom/matrix-client-commet/templates/global_config.json.j2 b/roles/custom/matrix-client-commet/templates/global_config.json.j2 new file mode 100644 index 000000000..3e156ccd5 --- /dev/null +++ b/roles/custom/matrix-client-commet/templates/global_config.json.j2 @@ -0,0 +1,3 @@ +{ + "default_homeserver": "{{ matrix_client_commet_default_homeserver }}" +} diff --git a/roles/custom/matrix-client-commet/templates/global_config.json.j2.license b/roles/custom/matrix-client-commet/templates/global_config.json.j2.license new file mode 100644 index 000000000..d6f441299 --- /dev/null +++ b/roles/custom/matrix-client-commet/templates/global_config.json.j2.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 MDAD project contributors + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/roles/custom/matrix-client-commet/templates/labels.j2 b/roles/custom/matrix-client-commet/templates/labels.j2 new file mode 100644 index 000000000..a1bab621c --- /dev/null +++ b/roles/custom/matrix-client-commet/templates/labels.j2 @@ -0,0 +1,60 @@ +{# +SPDX-FileCopyrightText: 2026 MDAD project contributors + +SPDX-License-Identifier: AGPL-3.0-or-later +#} + +{# +Traefik labels for matrix-client-commet. +#} + +{% if matrix_client_commet_container_labels_traefik_enabled %} +traefik.enable=true + +{% if matrix_client_commet_container_labels_traefik_docker_network %} +traefik.docker.network={{ matrix_client_commet_container_labels_traefik_docker_network }} +{% endif %} + +traefik.http.services.matrix-client-commet.loadbalancer.server.port={{ matrix_client_commet_container_port }} + +{% set middlewares = [] %} + +{% if matrix_client_commet_container_labels_traefik_compression_middleware_enabled %} +{% set middlewares = middlewares + [matrix_client_commet_container_labels_traefik_compression_middleware_name] %} +{% endif %} + +{% if matrix_client_commet_container_labels_traefik_path_prefix != '/' %} +traefik.http.middlewares.matrix-client-commet-slashless-redirect.redirectregex.regex=({{ matrix_client_commet_container_labels_traefik_path_prefix | quote }})$ +traefik.http.middlewares.matrix-client-commet-slashless-redirect.redirectregex.replacement=${1}/ +{% set middlewares = middlewares + ['matrix-client-commet-slashless-redirect'] %} +{% endif %} + +{% if matrix_client_commet_container_labels_traefik_path_prefix != '/' %} +traefik.http.middlewares.matrix-client-commet-strip-prefix.stripprefix.prefixes={{ matrix_client_commet_container_labels_traefik_path_prefix }} +{% set middlewares = middlewares + ['matrix-client-commet-strip-prefix'] %} +{% endif %} + +{% if matrix_client_commet_container_labels_traefik_additional_response_headers.keys() | length > 0 %} +{% for name, value in matrix_client_commet_container_labels_traefik_additional_response_headers.items() %} +traefik.http.middlewares.matrix-client-commet-add-headers.headers.customresponseheaders.{{ name }}={{ value }} +{% endfor %} +{% set middlewares = middlewares + ['matrix-client-commet-add-headers'] %} +{% endif %} + +traefik.http.routers.matrix-client-commet.rule={{ matrix_client_commet_container_labels_traefik_rule }} +{% if matrix_client_commet_container_labels_traefik_priority | int > 0 %} +traefik.http.routers.matrix-client-commet.priority={{ matrix_client_commet_container_labels_traefik_priority }} +{% endif %} +traefik.http.routers.matrix-client-commet.service=matrix-client-commet +{% if middlewares | length > 0 %} +traefik.http.routers.matrix-client-commet.middlewares={{ middlewares | join(',') }} +{% endif %} +traefik.http.routers.matrix-client-commet.entrypoints={{ matrix_client_commet_container_labels_traefik_entrypoints }} +traefik.http.routers.matrix-client-commet.tls={{ matrix_client_commet_container_labels_traefik_tls | to_json }} +{% if matrix_client_commet_container_labels_traefik_tls %} +traefik.http.routers.matrix-client-commet.tls.certResolver={{ matrix_client_commet_container_labels_traefik_tls_certResolver }} +{% endif %} + +{% endif %} + +{{ matrix_client_commet_container_labels_additional_labels }} diff --git a/roles/custom/matrix-client-commet/templates/systemd/matrix-client-commet.service.j2 b/roles/custom/matrix-client-commet/templates/systemd/matrix-client-commet.service.j2 new file mode 100644 index 000000000..adf998351 --- /dev/null +++ b/roles/custom/matrix-client-commet/templates/systemd/matrix-client-commet.service.j2 @@ -0,0 +1,58 @@ +{# +SPDX-FileCopyrightText: 2026 MDAD project contributors + +SPDX-License-Identifier: AGPL-3.0-or-later +#} + +#jinja2: lstrip_blocks: True +[Unit] +Description=Matrix Commet web client +{% for service in matrix_client_commet_systemd_required_services_list %} +Requires={{ service }} +After={{ service }} +{% endfor %} +DefaultDependencies=no + +[Service] +Type=simple +Environment="HOME={{ devture_systemd_docker_base_systemd_unit_home_path }}" +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-client-commet 2>/dev/null || true' +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-client-commet 2>/dev/null || true' + +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \ + --rm \ + --name=matrix-client-commet \ + --log-driver=none \ + --user={{ matrix_user_uid }}:{{ matrix_user_gid }} \ + --cap-drop=ALL \ + --read-only \ + --network={{ matrix_client_commet_container_network }} \ + {% if matrix_client_commet_container_http_host_bind_port %} + -p {{ matrix_client_commet_container_http_host_bind_port }}:{{ matrix_client_commet_container_port }} \ + {% endif %} + --label-file={{ matrix_client_commet_base_path }}/labels \ + --env-file={{ matrix_client_commet_base_path }}/env \ + --tmpfs=/tmp:rw,noexec,nosuid,size=10m \ + --tmpfs=/var/cache/nginx:rw,mode=777 \ + --tmpfs=/var/run:rw,mode=777 \ + --mount type=bind,src={{ matrix_client_commet_config_path }}/global_config.json,dst=/usr/share/nginx/html/assets/assets/config/global_config.json,ro \ + {% for arg in matrix_client_commet_container_extra_arguments %} + {{ arg }} \ + {% endfor %} + {{ matrix_client_commet_container_image }} + +{% for network in matrix_client_commet_container_additional_networks %} +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} network connect {{ network }} matrix-client-commet +{% endfor %} + +ExecStart={{ devture_systemd_docker_base_host_command_docker }} start --attach matrix-client-commet + +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-client-commet 2>/dev/null || true' +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-client-commet 2>/dev/null || true' + +Restart=always +RestartSec=30 +SyslogIdentifier=matrix-client-commet + +[Install] +WantedBy=multi-user.target diff --git a/setup.yml b/setup.yml index d3a40b1a1..60e926916 100644 --- a/setup.yml +++ b/setup.yml @@ -106,6 +106,7 @@ - custom/matrix-prometheus-services-connect - custom/matrix-registration - custom/matrix-client-element + - custom/matrix-client-commet - galaxy/hydrogen - galaxy/cinny - galaxy/sable