Optimized Docker Matrix Role in Preparation for use on CyMaIS.Cloud Server

This commit is contained in:
Kevin Veen-Birkenbach 2025-05-15 21:11:21 +02:00
parent 9c65c320f9
commit 76aef5949b
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
20 changed files with 219 additions and 84 deletions

View File

@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@ -1,3 +1,4 @@
CYMAIS_ENVIRONMENT: "production"
HOST_CURRENCY: "EUR"
HOST_TIMEZONE: "UTC"

View File

@ -12,7 +12,7 @@ SSH_PORT={{ports.public.ssh[application_id]}}
SSH_LISTEN_PORT=22
DOMAIN={{domains[application_id]}}
SSH_DOMAIN={{domains[application_id]}}
RUN_MODE="{{run_mode}}"
RUN_MODE="{{ 'dev' if (CYMAIS_ENVIRONMENT | lower) == 'development' else 'prod' }}"
ROOT_URL="{{ web_protocol }}://{{domains[application_id]}}/"
# Mail Configuration

View File

@ -14,4 +14,6 @@ features:
csp:
flags:
script-src:
unsafe-inline: true
style-src:
unsafe-inline: true

View File

@ -0,0 +1,5 @@
# Todo
- Enable Whatsapp by default
- Enable Telegram by default
- Enable Slack by default
- Enable ChatGPT by default

View File

@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@ -0,0 +1,13 @@
def filter_enabled_bridges(bridges, plugins):
"""
Return only those bridge definitions whose 'bridge_name' is set to True in plugins.
:param bridges: list of dicts, each with a 'bridge_name' key
:param plugins: dict mapping bridge_name to a boolean
"""
return [b for b in bridges if plugins.get(b['bridge_name'], False)]
class FilterModule(object):
def filters(self):
return {
'filter_enabled_bridges': filter_enabled_bridges,
}

View File

@ -1,4 +1,13 @@
---
- name: Load bridges configuration
include_vars:
file: "bridges.yml"
- name: Filter enabled bridges and register as fact
set_fact:
bridges: "{{ bridges_configuration | filter_enabled_bridges(applications[application_id].plugins) }}"
changed_when: false
- name: "include docker-central-database"
include_role:
name: docker-central-database

View File

@ -25,7 +25,11 @@ services:
interval: 1m
timeout: 10s
retries: 3
{% if bridges | bool %}
{% include 'templates/docker/container/depends-on-also-database.yml.j2' %}
{% else %}
{% include 'templates/docker/container/depends-on-just-database.yml.j2' %}
{% endif %}
{% for item in bridges %}
mautrix-{{item.bridge_name}}:
condition: service_healthy
@ -61,47 +65,47 @@ services:
retries: 3
{% include 'templates/docker/container/networks.yml.j2' %}
{% endfor %}
# Deactivated chatgpt.
# @todo needs to be reactivated as soon as bug is found
# matrix-chatgpt-bot:
# restart: {{docker_restart_policy}}
# container_name: matrix-chatgpt
# image: ghcr.io/matrixgpt/matrix-chatgpt-bot:latest
# volumes:
# - chatgpt_data:/storage
# environment:
# OPENAI_API_KEY: '{{applications[application_id].credentials.chatgpt_bridge_openai_api_key}}'
# # Uncomment the next two lines if you are using Azure OpenAI API
# # OPENAI_AZURE: 'false'
# # CHATGPT_REVERSE_PROXY: 'your-completion-endpoint-here'
# CHATGPT_CONTEXT: 'thread'
# CHATGPT_API_MODEL: 'gpt-3.5-turbo'
# # Uncomment and edit the next line if needed
# # CHATGPT_PROMPT_PREFIX: 'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.'
# # CHATGPT_IGNORE_MEDIA: 'false'
# CHATGPT_REVERSE_PROXY: 'https://api.openai.com/v1/chat/completions'
# # Uncomment and edit the next line if needed
# # CHATGPT_TEMPERATURE: '0.8'
# # Uncomment and edit the next line if needed
# #CHATGPT_MAX_CONTEXT_TOKENS: '4097'
# CHATGPT_MAX_PROMPT_TOKENS: '3000'
# KEYV_BACKEND: 'file'
# KEYV_URL: ''
# KEYV_BOT_ENCRYPTION: 'false'
# KEYV_BOT_STORAGE: 'true'
# MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}'
# MATRIX_BOT_USERNAME: '@chatgptbot:{{applications.matrix.server_name}}'
# MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}'
# MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}'
# MATRIX_DEFAULT_PREFIX: '!chatgpt'
# MATRIX_DEFAULT_PREFIX_REPLY: 'false'
# #MATRIX_BLACKLIST: ''
# MATRIX_WHITELIST: ':{{applications.matrix.server_name}}'
# MATRIX_AUTOJOIN: 'true'
# MATRIX_ENCRYPTION: 'true'
# MATRIX_THREADS: 'true'
# MATRIX_PREFIX_DM: 'false'
# MATRIX_RICH_TEXT: 'true'
{% if applications[application_id] | bool %}
matrix-chatgpt-bot:
restart: {{docker_restart_policy}}
container_name: matrix-chatgpt
image: ghcr.io/matrixgpt/matrix-chatgpt-bot:latest
volumes:
- chatgpt_data:/storage
environment:
OPENAI_API_KEY: '{{applications[application_id].credentials.chatgpt_bridge_openai_api_key}}'
# Uncomment the next two lines if you are using Azure OpenAI API
# OPENAI_AZURE: 'false'
# CHATGPT_REVERSE_PROXY: 'your-completion-endpoint-here'
CHATGPT_CONTEXT: 'thread'
CHATGPT_API_MODEL: 'gpt-3.5-turbo'
# Uncomment and edit the next line if needed
# CHATGPT_PROMPT_PREFIX: 'Instructions:\nYou are ChatGPT, a large language model trained by OpenAI.'
# CHATGPT_IGNORE_MEDIA: 'false'
CHATGPT_REVERSE_PROXY: 'https://api.openai.com/v1/chat/completions'
# Uncomment and edit the next line if needed
# CHATGPT_TEMPERATURE: '0.8'
# Uncomment and edit the next line if needed
#CHATGPT_MAX_CONTEXT_TOKENS: '4097'
CHATGPT_MAX_PROMPT_TOKENS: '3000'
KEYV_BACKEND: 'file'
KEYV_URL: ''
KEYV_BOT_ENCRYPTION: 'false'
KEYV_BOT_STORAGE: 'true'
MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}'
MATRIX_BOT_USERNAME: '@chatgptbot:{{applications.matrix.server_name}}'
MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}'
MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}'
MATRIX_DEFAULT_PREFIX: '!chatgpt'
MATRIX_DEFAULT_PREFIX_REPLY: 'false'
#MATRIX_BLACKLIST: ''
MATRIX_WHITELIST: ':{{applications.matrix.server_name}}'
MATRIX_AUTOJOIN: 'true'
MATRIX_ENCRYPTION: 'true'
MATRIX_THREADS: 'true'
MATRIX_PREFIX_DM: 'false'
MATRIX_RICH_TEXT: 'true'
{% endif %}
{% include 'templates/docker/compose/volumes.yml.j2' %}
synapse_data:

View File

@ -61,7 +61,9 @@ oidc_providers:
backchannel_logout_enabled: true
{% endif %}
{% if bridges | bool %}
app_service_config_files:
{% for item in bridges %}
- {{registration_file_folder}}{{item.bridge_name}}.registration.yaml
{% endfor %}
{% endfor %}
{% endif %}

View File

@ -0,0 +1,30 @@
bridges_configuration:
- database_password: "{{ applications[application_id].credentials.mautrix_whatsapp_bridge_database_password }}"
database_username: "mautrix_whatsapp_bridge"
database_name: "mautrix_whatsapp_bridge"
bridge_name: "whatsapp"
- database_password: "{{ applications[application_id].credentials.mautrix_telegram_bridge_database_password }}"
database_username: "mautrix_telegram_bridge"
database_name: "mautrix_telegram_bridge"
bridge_name: "telegram"
- database_password: "{{ applications[application_id].credentials.mautrix_signal_bridge_database_password }}"
database_username: "mautrix_signal_bridge"
database_name: "mautrix_signal_bridge"
bridge_name: "signal"
- database_password: "{{ applications[application_id].credentials.mautrix_slack_bridge_database_password }}"
database_username: "mautrix_slack_bridge"
database_name: "mautrix_slack_bridge"
bridge_name: "slack"
- database_password: "{{ applications[application_id].credentials.mautrix_facebook_bridge_database_password }}"
database_username: "mautrix_facebook_bridge"
database_name: "mautrix_facebook_bridge"
bridge_name: "facebook"
- database_password: "{{ applications[application_id].credentials.mautrix_instagram_bridge_database_password }}"
database_username: "mautrix_instagram_bridge"
database_name: "mautrix_instagram_bridge"
bridge_name: "instagram"

View File

@ -3,7 +3,6 @@ users:
administrator:
username: "{{users.administrator.username}}" # Accountname of the matrix admin
playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start
role: "compose" # Role to setup Matrix. Valid values: ansible, compose
server_name: "{{primary_domain}}" # Adress for the account names etc.
synapse:
version: "latest"
@ -30,3 +29,14 @@ csp:
script-src:
- "{{ domains.synapse }}"
- "https://cdn.jsdelivr.net"
plugins:
# You need to enable them in the inventory file
chatgpt: false
facebook: false
immesage: false
instagram: false
signal: false
slack: false
telegram: false
whatsapp: false

View File

@ -1,38 +1,5 @@
---
application_id: "matrix"
application_id: "matrix"
database_type: "postgres"
registration_file_folder: "/data/"
well_known_directory: "{{nginx.directories.data.well_known}}/matrix/"
bridges:
- database_password: "{{ applications[application_id].credentials.mautrix_whatsapp_bridge_database_password }}"
database_username: "mautrix_whatsapp_bridge"
database_name: "mautrix_whatsapp_bridge"
bridge_name: "whatsapp"
- database_password: "{{ applications[application_id].credentials.mautrix_telegram_bridge_database_password }}"
database_username: "mautrix_telegram_bridge"
database_name: "mautrix_telegram_bridge"
bridge_name: "telegram"
- database_password: "{{ applications[application_id].credentials.mautrix_signal_bridge_database_password }}"
database_username: "mautrix_signal_bridge"
database_name: "mautrix_signal_bridge"
bridge_name: "signal"
# Deactivated temporary, due to bug which is hard to find
# @todo Reactivate
# - database_password: "{{ applications[application_id].credentials.mautrix_slack_bridge_database_password }}"
# database_username: "mautrix_slack_bridge"
# database_name: "mautrix_slack_bridge"
# bridge_name: "slack"
- database_password: "{{ applications[application_id].credentials.mautrix_facebook_bridge_database_password }}"
database_username: "mautrix_facebook_bridge"
database_name: "mautrix_facebook_bridge"
bridge_name: "facebook"
- database_password: "{{ applications[application_id].credentials.mautrix_instagram_bridge_database_password }}"
database_username: "mautrix_instagram_bridge"
database_name: "mautrix_instagram_bridge"
bridge_name: "instagram"
well_known_directory: "{{nginx.directories.data.well_known}}/matrix/"

View File

@ -96,7 +96,7 @@
shell: >
docker compose exec web bash -c "
cd /app &&
RAILS_ENV=production bundle exec rails runner \"
RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bundle exec rails runner \"
user = User.find_by(mail: '{{ users.administrator.email }}');
if user.nil?;
puts 'User with email {{ users.administrator.email }} not found.';

View File

@ -49,7 +49,7 @@
- name: Set settings in OpenProject
shell: >
docker compose exec web bash -c "cd /app &&
RAILS_ENV=production bundle exec rails runner \"Setting[:{{ item.key }}] = '{{ item.value }}'\""
RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bundle exec rails runner \"Setting[:{{ item.key }}] = '{{ item.value }}'\""
args:
chdir: "{{ docker_compose.directories.instance }}"
loop: "{{ openproject_rails_settings | dict2items }}"

View File

@ -149,7 +149,7 @@ def update_mastodon():
Runs the database migration for Mastodon to ensure all required tables are up to date.
"""
print("Starting Mastodon database migration.")
run_command("docker compose exec -T web bash -c 'RAILS_ENV=production bin/rails db:migrate'")
run_command("docker compose exec -T web bash -c 'RAILS_ENV={{ CYMAIS_ENVIRONMENT | lower }} bin/rails db:migrate'")
print("Mastodon database migration complete.")
def upgrade_listmonk():

View File

@ -0,0 +1,64 @@
import os
import sys
import unittest
# Add the filter_plugins directory from the docker-matrix role to the import path
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../roles/docker-matrix")
),
)
from filter_plugins.bridge_filters import filter_enabled_bridges
class TestBridgeFilters(unittest.TestCase):
def test_no_bridges_returns_empty_list(self):
# When there are no bridges defined, result should be an empty list
self.assertEqual(filter_enabled_bridges([], {}), [])
def test_all_bridges_disabled(self):
# Define two bridges, but plugins dict has them disabled or missing
bridges = [
{'bridge_name': 'whatsapp', 'config': {}},
{'bridge_name': 'telegram', 'config': {}},
]
plugins = {
'whatsapp': False,
'telegram': False,
}
result = filter_enabled_bridges(bridges, plugins)
self.assertEqual(result, [])
def test_some_bridges_enabled(self):
# Only bridges with True in plugins should be returned
bridges = [
{'bridge_name': 'whatsapp', 'version': '1.0'},
{'bridge_name': 'telegram', 'version': '1.0'},
{'bridge_name': 'signal', 'version': '1.0'},
]
plugins = {
'whatsapp': True,
'telegram': False,
'signal': True,
}
result = filter_enabled_bridges(bridges, plugins)
expected = [
{'bridge_name': 'whatsapp', 'version': '1.0'},
{'bridge_name': 'signal', 'version': '1.0'},
]
self.assertEqual(result, expected)
def test_bridge_without_plugin_entry_defaults_to_disabled(self):
# If a bridge_name is not present in plugins, it should be treated as disabled
bridges = [
{'bridge_name': 'facebook', 'enabled': True},
]
plugins = {} # no entries
result = filter_enabled_bridges(bridges, plugins)
self.assertEqual(result, [])
if __name__ == "__main__":
unittest.main()

View File

@ -1,6 +1,16 @@
# tests/unit/test_configuration_filters.py
import unittest
import sys
import os
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.configuration_filters import (
is_feature_enabled,
)

View File

@ -1,6 +1,16 @@
import unittest
import hashlib
import base64
import sys
import os
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.csp_filters import FilterModule, AnsibleFilterError
class TestCspFilters(unittest.TestCase):

View File

@ -2,8 +2,12 @@ import os
import sys
import unittest
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
sys.path.insert(0, PROJECT_ROOT)
sys.path.insert(
0,
os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../")
),
)
from filter_plugins.redirect_filters import FilterModule