ejabberd: STUN/TURN configuration

This implementation is very similar to that of Matrix Synapse with a lot
of code duplicated.

One major difference is that ejabberd doesn't have a conf.d/ directory.
So, the managed configuration and overridden configuration cannot be
cleanly separated.

Whether the configuration is managed or not is determined by the
presence of a file under `/etc/ejabberd`. Managed coturn configuration
isn't stored in ejabberd, since only one set of configuration can be
stored at a time. If the admin chooses to use the managed configuration,
the current coturn configuration is fetched and used to configure
ejabberd.

Fixes #1978

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2021-03-05 22:08:56 +05:30 committed by James Valleroy
parent d70816d0e5
commit 7a30470cb5
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
10 changed files with 622 additions and 30 deletions

View File

@ -7,20 +7,22 @@ Configuration helper for the ejabberd service
import argparse
import json
import os
import pathlib
import re
import shutil
import socket
import subprocess
import sys
from distutils.version import LooseVersion as LV
from ruamel.yaml import YAML, scalarstring
from pathlib import Path
from plinth import action_utils
from ruamel.yaml import YAML, scalarstring
EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem'
EJABBERD_MANAGED_COTURN = '/etc/ejabberd/freedombox_managed_coturn'
IQDISC_DEPRECATED_VERSION = LV('18.03')
MOD_IRC_DEPRECATED_VERSION = LV('18.06')
@ -70,6 +72,18 @@ def parse_arguments():
help='Add a domain name to ejabberd')
add_domain.add_argument('--domainname', help='New domain name')
# Configure STUN/TURN server for use with ejabberd
turn = subparsers.add_parser(
'configure-turn', help='Configure a TURN server for use with ejabberd')
turn.add_argument(
'--managed', required=False, default=False, action='store_true',
help='Whether configuration is provided by user or auto-managed by '
'FreedomBox')
subparsers.add_parser(
'get-turn-config',
help='Get the latest STUN/TURN configuration in JSON format')
# Switch/check Message Archive Management (MAM) in ejabberd config
help_MAM = 'Switch or check Message Archive Management (MAM).'
mam = subparsers.add_parser('mam', help=help_MAM)
@ -107,6 +121,8 @@ def subcommand_setup(arguments):
for listen_port in conf['listen']:
if 'tls' in listen_port:
listen_port['tls'] = False
if 'use_turn' in listen_port:
conf['listen'].remove(listen_port) # Use coturn instead
conf['auth_method'] = 'ldap'
conf['ldap_servers'] = [scalarstring.DoubleQuotedScalarString('localhost')]
@ -154,7 +170,7 @@ def upgrade_config(domain):
if listen_port['port'] == 5280:
listen_port['port'] = 5443
cert_dir = pathlib.Path('/etc/ejabberd/letsencrypt') / domain
cert_dir = Path('/etc/ejabberd/letsencrypt') / domain
cert_file = str(cert_dir / 'ejabberd.pem')
cert_file = scalarstring.DoubleQuotedScalarString(cert_file)
conf['s2s_certfile'] = cert_file
@ -285,6 +301,81 @@ def subcommand_mam(argument):
subprocess.call(['ejabberdctl', 'reload_config'])
def _generate_service(uri: str) -> dict:
"""Generate ejabberd mod_stun_disco service config from Coturn URI."""
pattern = re.compile(r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)')
typ, domain, port, transport = pattern.match(uri).groups()
return {
"host": domain,
"port": int(port),
"type": typ,
"transport": transport,
"restricted": False
}
def _generate_uris(services: list[dict]) -> list[str]:
"""Generate STUN/TURN URIs from ejabberd mod_stun_disco service config."""
return [
f"{s['type']}:{s['host']}:{s['port']}?transport={s['transport']}"
for s in services
]
def subcommand_get_turn_config(_):
"""Get the latest STUN/TURN configuration in JSON format."""
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = yaml.load(file_handle)
mod_stun_disco_config = conf['modules']['mod_stun_disco']
managed = os.path.exists(EJABBERD_MANAGED_COTURN)
if bool(mod_stun_disco_config):
print(
json.dumps([{
'domain': '',
'uris': _generate_uris(mod_stun_disco_config['services']),
'shared_secret': mod_stun_disco_config['secret'],
}, managed]))
else:
print(
json.dumps([{
'domain': None,
'uris': [],
'shared_secret': None
}, managed]))
def subcommand_configure_turn(arguments):
"""Set parameters for the STUN/TURN server to use with ejabberd."""
turn_server_config = json.loads(''.join(sys.stdin))
uris = turn_server_config['uris']
mod_stun_disco_config = {}
if turn_server_config['uris'] and turn_server_config['shared_secret']:
mod_stun_disco_config = {
'credentials_lifetime': '1000d',
'secret': turn_server_config['shared_secret'],
'services': [_generate_service(uri) for uri in uris]
}
with open(EJABBERD_CONFIG, 'r') as file_handle:
conf = yaml.load(file_handle)
conf['modules']['mod_stun_disco'] = mod_stun_disco_config
with open(EJABBERD_CONFIG, 'w') as file_handle:
yaml.dump(conf, file_handle)
if arguments.managed:
Path(EJABBERD_MANAGED_COTURN).touch()
else:
Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True)
if action_utils.service_is_running('ejabberd'):
subprocess.call(['ejabberdctl', 'reload_config'])
def _get_version():
""" Get the current ejabberd version """
try:

View File

@ -2,11 +2,10 @@
"""App component for other apps to manage their STUN/TURN server configuration.
"""
from __future__ import annotations
from __future__ import annotations # Can be removed in Python 3.10
import json
from dataclasses import dataclass, field
from typing import List
from plinth import app
@ -27,15 +26,15 @@ class TurnConfiguration:
"""
domain: str = None
uris: List[str] = field(default_factory=list)
uris: list[str] = field(default_factory=list)
shared_secret: str = None
def __post_init__(self):
"""Generate URIs after object initialization if necessary."""
if self.domain and not self.uris:
self.uris = [
f'{proto1}:{self.domain}:3478?transport={proto2}'
for proto1 in ['stun', 'turn'] for proto2 in ['tcp', 'udp']
f'{typ}:{self.domain}:3478?transport={transport}'
for typ in ['stun', 'turn'] for transport in ['tcp', 'udp']
]
def to_json(self) -> str:
@ -73,7 +72,7 @@ class TurnConsumer(app.FollowerComponent):
self._all[component_id] = self
@classmethod
def list(cls) -> List[TurnConsumer]: # noqa
def list(cls) -> list[TurnConsumer]: # noqa
"""Return a list of all Coturn components."""
return cls._all.values()

View File

@ -9,7 +9,6 @@ import pathlib
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import cfg, frontpage, menu
@ -17,6 +16,7 @@ from plinth.daemon import Daemon
from plinth.modules import config
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.coturn.components import TurnConfiguration, TurnConsumer
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.modules.users.components import UsersAndGroups
@ -26,7 +26,7 @@ from plinth.utils import format_lazy
from . import manifest
version = 3
version = 4
managed_services = ['ejabberd']
@ -44,9 +44,15 @@ _description = [
'When enabled, ejabberd can be accessed by any '
'<a href="{users_url}"> user with a {box_name} login</a>.'),
box_name=_(cfg.box_name), users_url=reverse_lazy('users:index'),
jsxc_url=reverse_lazy('jsxc:index'))
jsxc_url=reverse_lazy('jsxc:index')),
format_lazy(
_('ejabberd needs a STUN/TURN server for audio/video calls. '
'Install the <a href={coturn_url}>Coturn</a> app or configure '
'an external server.'), coturn_url=reverse_lazy('coturn:index'))
]
depends = ['coturn']
logger = logging.getLogger(__name__)
app = None
@ -113,11 +119,22 @@ class EjabberdApp(app_module.App):
**manifest.backup)
self.add(backup_restore)
turn = EjabberdTurnConsumer('turn-ejabberd')
self.add(turn)
pre_hostname_change.connect(on_pre_hostname_change)
post_hostname_change.connect(on_post_hostname_change)
domain_added.connect(on_domain_added)
class EjabberdTurnConsumer(TurnConsumer):
"""Component to manage Coturn configuration for ejabberd."""
def on_config_change(self, config):
"""Add or update STUN/TURN configuration."""
update_turn_configuration(config)
def setup(helper, old_version=None):
"""Install and configure the module."""
domainname = config.get_domainname()
@ -134,6 +151,10 @@ def setup(helper, old_version=None):
['setup', '--domainname', domainname])
helper.call('post', app.enable)
# Configure STUN/TURN only if there's a valid TLS domain set for Coturn
configuration = app.get_component('turn-ejabberd').get_configuration()
update_turn_configuration(configuration, force=True)
def get_domains():
"""Return the list of domains that ejabberd is interested in.
@ -194,3 +215,23 @@ def on_domain_added(sender, domain_type, name, description='', services=None,
if name not in conf['domains']:
actions.superuser_run('ejabberd', ['add-domain', '--domainname', name])
app.get_component('letsencrypt-ejabberd').setup_certificates()
def update_turn_configuration(config: TurnConfiguration, managed=True,
force=False):
"""Update ejabberd's STUN/TURN server configuration."""
setup_helper = globals()['setup_helper']
if not force and setup_helper.get_state() == 'needs-setup':
return
params = ['configure-turn']
params += ['--managed'] if managed else []
actions.superuser_run('ejabberd', params, input=config.to_json().encode())
def get_turn_configuration() -> (TurnConfiguration, bool):
"""Get the latest STUN/TURN configuration."""
json_config = actions.superuser_run('ejabberd', ['get-turn-config'])
tc, managed = json.loads(json_config)
return (TurnConfiguration(tc['domain'], tc['uris'],
tc['shared_secret']), managed)

View File

@ -4,6 +4,7 @@ Forms for configuring Ejabberd.
"""
from django import forms
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from plinth import cfg
@ -20,3 +21,28 @@ class EjabberdForm(forms.Form):
'clients, and reading the history of a multi-user chat room. '
'It depends on the client settings whether the histories are '
'stored as plain text or encrypted.'), box_name=_(cfg.box_name)))
enable_managed_turn = forms.BooleanField(
label=_('Automatically manage audio/video call setup'), required=False,
help_text=format_lazy(
_('Configures the local <a href={coturn_url}>coturn</a> app as '
'the STUN/TURN server for ejabberd. Disable this if you '
'want to use a different STUN/TURN server.'),
coturn_url=reverse_lazy('coturn:index')))
# STUN/TURN server setup
turn_uris = forms.CharField(
label=_('STUN/TURN Server URIs'), required=False, strip=True,
widget=forms.Textarea(attrs={'rows': 4}),
help_text=_('List of public URIs of the STUN/TURN server, one on each '
'line.'))
shared_secret = forms.CharField(
label=_('Shared Authentication Secret'), required=False, strip=True,
help_text=_('Shared secret used to compute passwords for the '
'TURN server.'))
def clean_turn_uris(self):
"""Normalize newlines in URIs."""
data = self.cleaned_data['turn_uris']
return '\n'.join([uri.strip() for uri in data.splitlines()])

View File

@ -0,0 +1,35 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* @licstart The following is the entire license notice for the JavaScript
* code in this page.
*
* This file is part of FreedomBox.
*
* 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 by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice for the JavaScript code
* in this page.
*/
jQuery(function($) {
$('#id_enable_managed_turn').change(function() {
if($(this).prop('checked')) {
$('#id_turn_uris').closest('.form-group').hide();
$('#id_shared_secret').closest('.form-group').hide();
} else {
$('#id_turn_uris').closest('.form-group').show();
$('#id_shared_secret').closest('.form-group').show();
}
}).change();
});

View File

@ -3,8 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block description %}
@ -28,3 +29,8 @@
</p>
{% endblock %}
{% block page_js %}
<script type="text/javascript"
src="{% static 'ejabberd/ejabberd.js' %}"></script>
{% endblock %}

View File

@ -0,0 +1,78 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Common test fixtures for ejabberd.
"""
import importlib
import pathlib
import shutil
import types
from unittest.mock import MagicMock, patch
import pytest
from plinth.modules import ejabberd
current_directory = pathlib.Path(__file__).parent
def _load_actions_module():
actions_file_path = str(current_directory / '..' / '..' / '..' / '..' /
'actions' / 'ejabberd')
loader = importlib.machinery.SourceFileLoader('ejabberd',
actions_file_path)
module = types.ModuleType(loader.name)
loader.exec_module(module)
return module
actions = _load_actions_module()
@pytest.fixture(name='conf_file')
def fixture_conf_file(tmp_path):
"""Uses a dummy configuration file."""
settings_file_name = 'ejabberd.yml'
conf_file = tmp_path / settings_file_name
conf_file.touch()
shutil.copyfile(str(current_directory / 'data' / 'ejabberd.yml.example'),
str(conf_file))
return str(conf_file)
@pytest.fixture(name='managed_file')
def fixture_managed_file(tmp_path):
"""Uses a dummy managed file."""
file_name = 'freedombox_managed_coturn'
fil = tmp_path / file_name
return str(fil)
@pytest.fixture(name='call_action')
def fixture_call_action(capsys, conf_file, managed_file):
"""Run actions with custom root path."""
def _call_action(module_name, args, **kwargs):
actions.EJABBERD_CONFIG = conf_file
actions.EJABBERD_MANAGED_COTURN = managed_file
with patch('argparse._sys.argv', [module_name] + args):
actions.main()
captured = capsys.readouterr()
return captured.out
return _call_action
@pytest.fixture(name='test_configuration', autouse=True)
def fixture_test_configuration(call_action, conf_file):
"""Use a separate ejabberd configuration for tests.
Patches actions.superuser_run with the fixture call_action.
The module state is patched to be 'up-to-date'.
"""
with patch('plinth.actions.superuser_run', call_action):
helper = MagicMock()
helper.get_state.return_value = 'up-to-date'
ejabberd.setup_helper = helper
yield

View File

@ -0,0 +1,231 @@
###
### ejabberd configuration file
###
### The parameters used in this configuration file are explained at
###
### https://docs.ejabberd.im/admin/configuration
###
### The configuration file is written in YAML.
### *******************************************************
### ******* !!! WARNING !!! *******
### ******* YAML IS INDENTATION SENSITIVE *******
### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY *******
### *******************************************************
### Refer to http://en.wikipedia.org/wiki/YAML for the brief description.
###
hosts:
- localhost
loglevel: info
## If you already have certificates, list them here
# certfiles:
# - /etc/letsencrypt/live/domain.tld/fullchain.pem
# - /etc/letsencrypt/live/domain.tld/privkey.pem
listen:
-
port: 5222
ip: "::"
module: ejabberd_c2s
max_stanza_size: 262144
shaper: c2s_shaper
access: c2s
starttls_required: true
-
port: 5269
ip: "::"
module: ejabberd_s2s_in
max_stanza_size: 524288
-
port: 5443
ip: "::"
module: ejabberd_http
tls: true
request_handlers:
/admin: ejabberd_web_admin
/api: mod_http_api
/bosh: mod_bosh
/captcha: ejabberd_captcha
/upload: mod_http_upload
/ws: ejabberd_http_ws
-
port: 5280
ip: "::"
module: ejabberd_http
request_handlers:
/admin: ejabberd_web_admin
/.well-known/acme-challenge: ejabberd_acme
-
port: 3478
ip: "::"
transport: udp
module: ejabberd_stun
use_turn: true
## The server's public IPv4 address:
# turn_ipv4_address: "203.0.113.3"
## The server's public IPv6 address:
# turn_ipv6_address: "2001:db8::3"
-
port: 1883
ip: "::"
module: mod_mqtt
backlog: 1000
s2s_use_starttls: optional
acl:
local:
user_regexp: ""
loopback:
ip:
- 127.0.0.0/8
- ::1/128
access_rules:
local:
allow: local
c2s:
deny: blocked
allow: all
announce:
allow: admin
configure:
allow: admin
muc_create:
allow: local
pubsub_createnode:
allow: local
trusted_network:
allow: loopback
api_permissions:
"console commands":
from:
- ejabberd_ctl
who: all
what: "*"
"admin access":
who:
access:
allow:
- acl: loopback
- acl: admin
oauth:
scope: "ejabberd:admin"
access:
allow:
- acl: loopback
- acl: admin
what:
- "*"
- "!stop"
- "!start"
"public commands":
who:
ip: 127.0.0.1/8
what:
- status
- connected_users_number
shaper:
normal:
rate: 3000
burst_size: 20000
fast: 100000
shaper_rules:
max_user_sessions: 10
max_user_offline_messages:
5000: admin
100: all
c2s_shaper:
none: admin
normal: all
s2s_shaper: fast
modules:
mod_adhoc: {}
mod_admin_extra: {}
mod_announce:
access: announce
mod_avatar: {}
mod_blocking: {}
mod_bosh: {}
mod_caps: {}
mod_carboncopy: {}
mod_client_state: {}
mod_configure: {}
mod_disco: {}
mod_fail2ban: {}
mod_http_api: {}
mod_http_upload:
put_url: https://@HOST@:5443/upload
custom_headers:
"Access-Control-Allow-Origin": "https://@HOST@"
"Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS"
"Access-Control-Allow-Headers": "Content-Type"
mod_last: {}
mod_mam:
## Mnesia is limited to 2GB, better to use an SQL backend
## For small servers SQLite is a good fit and is very easy
## to configure. Uncomment this when you have SQL configured:
## db_type: sql
assume_mam_usage: true
default: always
mod_mqtt: {}
mod_muc:
access:
- allow
access_admin:
- allow: admin
access_create: muc_create
access_persistent: muc_create
access_mam:
- allow
default_room_options:
mam: true
mod_muc_admin: {}
mod_offline:
access_max_user_messages: max_user_offline_messages
mod_ping: {}
mod_privacy: {}
mod_private: {}
mod_proxy65:
access: local
max_connections: 5
mod_pubsub:
access_createnode: pubsub_createnode
plugins:
- flat
- pep
force_node_config:
## Avoid buggy clients to make their bookmarks public
storage:bookmarks:
access_model: whitelist
mod_push: {}
mod_push_keepalive: {}
mod_register:
## Only accept registration requests from the "trusted"
## network (see access_rules section above).
## Think twice before enabling registration from any
## address. See the Jabber SPAM Manifesto for details:
## https://github.com/ge0rg/jabber-spam-fighting-manifesto
ip_access: trusted_network
mod_roster:
versioning: true
mod_s2s_dialback: {}
mod_shared_roster: {}
mod_stream_mgmt:
resend_on_timeout: if_offline
mod_stun_disco: {}
mod_vcard: {}
mod_vcard_xupdate: {}
mod_version:
show_os: false
### Local Variables:
### mode: yaml
### End:
### vim: set filetype=yaml tabstop=8

View File

@ -0,0 +1,48 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Test module for ejabberd STUN/TURN configuration.
"""
from unittest.mock import patch
from plinth.modules import ejabberd
from plinth.modules.coturn.components import TurnConfiguration
managed_configuration = TurnConfiguration(
'freedombox.local', [],
'aiP02OAGyOlj6WGuCyqj7iaOsbkC7BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51')
overridden_configuration = TurnConfiguration(
'public.coturn.site', [],
'BUeKvKzhAsTZ8MEwMd3yTwpr2uvbOxgWe51aiP02OAGyOlj6WGuCyqj7iaOsbkC7')
def _set_turn_configuration(monkeypatch, config=managed_configuration,
managed=True):
monkeypatch.setattr('sys.stdin', config.to_json())
with patch('plinth.action_utils.service_is_running', return_value=False):
ejabberd.update_turn_configuration(config, managed=managed)
def _assert_conf(expected_configuration, expected_managed):
"""Assert that ejabberd TURN configuration is as expected."""
config, managed = ejabberd.get_turn_configuration()
assert config.uris == expected_configuration.uris
assert config.shared_secret == expected_configuration.shared_secret
assert managed == expected_managed
def test_managed_turn_server_configuration(monkeypatch):
_set_turn_configuration(monkeypatch)
_assert_conf(managed_configuration, True)
def test_overridden_turn_server_configuration(monkeypatch):
_set_turn_configuration(monkeypatch, overridden_configuration, False)
_assert_conf(overridden_configuration, False)
def test_remove_turn_configuration(monkeypatch):
_set_turn_configuration(monkeypatch)
_set_turn_configuration(monkeypatch, TurnConfiguration(), False)
_assert_conf(TurnConfiguration(), False)

View File

@ -3,11 +3,12 @@
Views for the Ejabberd module
"""
from plinth.modules.coturn.components import TurnConfiguration
from django.contrib import messages
from django.utils.translation import ugettext as _
from plinth import actions
from plinth.modules import ejabberd
from plinth.modules import coturn, ejabberd
from plinth.views import AppView
from .forms import EjabberdForm
@ -20,9 +21,14 @@ class EjabberdAppView(AppView):
form_class = EjabberdForm
def get_initial(self):
initdict = super().get_initial()
initdict.update({'MAM_enabled': self.is_MAM_enabled()})
return initdict
"""Initial data to fill in the form."""
config, managed = ejabberd.get_turn_configuration()
return super().get_initial() | {
'MAM_enabled': self.is_MAM_enabled(),
'enable_managed_turn': managed,
'turn_uris': '\n'.join(config.uris),
'shared_secret': config.shared_secret
}
def get_context_data(self, *args, **kwargs):
"""Add service to the context data."""
@ -31,21 +37,52 @@ class EjabberdAppView(AppView):
context['domainname'] = domains[0] if domains else None
return context
@staticmethod
def _handle_turn_configuration(old_config, new_config):
if not new_config['enable_managed_turn']:
new_turn_uris = new_config['turn_uris'].splitlines()
new_shared_secret = new_config['shared_secret']
turn_config_changed = \
old_config['turn_uris'] != new_turn_uris or \
old_config['shared_secret'] != new_shared_secret
if turn_config_changed:
ejabberd.update_turn_configuration(
TurnConfiguration(None, new_turn_uris, new_shared_secret),
managed=False)
else:
ejabberd.update_turn_configuration(coturn.get_config(),
managed=True)
@staticmethod
def _handle_MAM_configuration(old_config, new_config):
# note ejabberd action "enable" or "disable" restarts, if running
if new_config['MAM_enabled']:
actions.superuser_run('ejabberd', ['mam', 'enable'])
else:
actions.superuser_run('ejabberd', ['mam', 'disable'])
def form_valid(self, form):
"""Enable/disable a service and set messages."""
old_status = form.initial
new_status = form.cleaned_data
old_config = form.initial
new_config = form.cleaned_data
if old_status['MAM_enabled'] != new_status['MAM_enabled']:
# note ejabberd action "enable" or "disable" restarts, if running
if new_status['MAM_enabled']:
actions.superuser_run('ejabberd', ['mam', 'enable'])
messages.success(self.request,
_('Message Archive Management enabled'))
else:
actions.superuser_run('ejabberd', ['mam', 'disable'])
messages.success(self.request,
_('Message Archive Management disabled'))
def changed(prop):
return old_config[prop] != new_config[prop]
is_changed = False
if changed('MAM_enabled'):
self._handle_MAM_configuration(old_config, new_config)
is_changed = True
if changed('enable_managed_turn') or changed('turn_uris') or \
changed('shared_secret'):
self._handle_turn_configuration(old_config, new_config)
is_changed = True
if is_changed:
messages.success(self.request, _('Configuration updated'))
return super().form_valid(form)