diff --git a/message_center/admin.py b/message_center/admin.py
index 8c38f3f..6c0e5cb 100644
--- a/message_center/admin.py
+++ b/message_center/admin.py
@@ -1,3 +1,42 @@
from django.contrib import admin
+from .models import Notification, MessageChannel, Message, MessageSettings
-# Register your models here.
+
+class NotificationAdmin(admin.ModelAdmin):
+ list_display = ('notification_type', 'router', 'router_backup', 'created', 'updated', 'uuid')
+ list_filter = ('notification_type', 'router', 'router_backup', 'created', 'updated', 'uuid')
+ search_fields = ('notification_type', 'router', 'router_backup', 'created', 'updated', 'uuid')
+ readonly_fields = ('created', 'updated', 'uuid')
+
+
+admin.site.register(Notification, NotificationAdmin)
+
+
+class MessageChannelAdmin(admin.ModelAdmin):
+ list_display = ('name', 'enabled', 'channel_type', 'destination', 'token', 'status_change_offline', 'status_change_online', 'backup_fail', 'daily_status_report', 'daily_backup_report', 'created', 'updated', 'uuid')
+ list_filter = ('name', 'enabled', 'channel_type', 'destination', 'token', 'status_change_offline', 'status_change_online', 'backup_fail', 'daily_status_report', 'daily_backup_report', 'created', 'updated', 'uuid')
+ search_fields = ('name', 'enabled', 'channel_type', 'destination', 'token', 'status_change_offline', 'status_change_online', 'backup_fail', 'daily_status_report', 'daily_backup_report', 'created', 'updated', 'uuid')
+ readonly_fields = ('created', 'updated', 'uuid')
+
+
+admin.site.register(MessageChannel, MessageChannelAdmin)
+
+
+class MessageAdmin(admin.ModelAdmin):
+ list_display = ('channel', 'subject', 'message', 'status', 'retry_count', 'error_message', 'completed', 'created', 'updated', 'uuid')
+ list_filter = ('channel', 'subject', 'message', 'status', 'retry_count', 'error_message', 'completed', 'created', 'updated', 'uuid')
+ search_fields = ('channel', 'subject', 'message', 'status', 'retry_count', 'error_message', 'completed', 'created', 'updated', 'uuid')
+ readonly_fields = ('created', 'updated', 'uuid')
+
+
+admin.site.register(Message, MessageAdmin)
+
+
+class MessageSettingsAdmin(admin.ModelAdmin):
+ list_display = ('name', 'max_length', 'max_retry', 'retry_interval', 'concatenate_status_change', 'created', 'updated', 'uuid')
+ list_filter = ('name', 'max_length', 'max_retry', 'retry_interval', 'concatenate_status_change', 'created', 'updated', 'uuid')
+ search_fields = ('name', 'max_length', 'max_retry', 'retry_interval', 'concatenate_status_change', 'created', 'updated', 'uuid')
+ readonly_fields = ('created', 'updated', 'uuid')
+
+
+admin.site.register(MessageSettings, MessageSettingsAdmin)
diff --git a/message_center/forms.py b/message_center/forms.py
new file mode 100644
index 0000000..904d34b
--- /dev/null
+++ b/message_center/forms.py
@@ -0,0 +1,173 @@
+import requests
+from .models import MessageSettings, MessageChannel
+
+from django import forms
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit, Div, Field, HTML
+from crispy_forms.bootstrap import FormActions, StrictButton
+from django.core.exceptions import ValidationError
+from datetime import datetime
+
+
+class MessageSettingsForm(forms.ModelForm):
+ class Meta:
+ model = MessageSettings
+ fields = [
+ 'max_length', 'max_retry', 'retry_interval', 'concatenate_status_change', 'status_change_delay',
+ 'concatenate_backup_fails', 'backup_fails_delay', 'daily_report_time'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(MessageSettingsForm, self).__init__(*args, **kwargs)
+ self.helper = FormHelper()
+ self.helper.layout = Layout(
+ Fieldset(
+ 'Message Settings',
+ ),
+ Div(
+ Div(Field('max_length'), css_class='col-md-6'),
+ Div(Field('daily_report_time'), css_class='col-md-6'),
+ css_class='row'),
+ Div(
+ Div(Field('max_retry'), css_class='col-md-6'),
+ Div(Field('retry_interval'), css_class='col-md-6'),
+ css_class='row'),
+ Div(
+ Div(
+ Div(
+ Div(Field('concatenate_status_change'), css_class='col-md-12'),
+ Div(Field('status_change_delay'), css_class='col-md-12'),
+ css_class='row'),
+ css_class='col-md-6'),
+ Div(
+ Div(
+ Div(Field('concatenate_backup_fails'), css_class='col-md-12'),
+ Div(Field('backup_fails_delay'), css_class='col-md-12'),
+ css_class='row'),
+ css_class='col-md-6'),
+ css_class='row'),
+ Div(
+ Div(
+ Submit('submit', 'Save', css_class='btn btn-success'),
+ HTML(' Back '),
+ css_class='col-md-12'
+ ),
+ css_class='row')
+ )
+
+ def clean(self):
+ cleaned_data = super().clean()
+
+ max_length = cleaned_data.get('max_length')
+ if max_length is not None and (max_length < 500 or max_length > 2000):
+ self.add_error('max_length', 'Max length must be between 500 and 2000.')
+
+ daily_report_time = cleaned_data.get('daily_report_time')
+ if daily_report_time is not None:
+ try:
+ datetime.strptime(daily_report_time, '%H:%M')
+ except ValueError:
+ self.add_error('daily_report_time', 'Invalid time format. Use HH:MM.')
+
+ max_retry = cleaned_data.get('max_retry')
+ if max_retry is not None and (max_retry < 0 or max_retry > 5):
+ self.add_error('max_retry', 'Max retry must be between 0 and 5.')
+
+ retry_interval = cleaned_data.get('retry_interval')
+ if retry_interval is not None and (retry_interval < 30 or retry_interval > 600):
+ self.add_error('retry_interval', 'Retry interval must be between 30 and 600 seconds.')
+
+ status_change_delay = cleaned_data.get('status_change_delay')
+ if status_change_delay is not None and (status_change_delay < 60 or status_change_delay > 600):
+ self.add_error('status_change_delay', 'Status change delay must be between 60 and 600 seconds.')
+
+ backup_fails_delay = cleaned_data.get('backup_fails_delay')
+ if backup_fails_delay is not None and (backup_fails_delay < 60 or backup_fails_delay > 3600):
+ self.add_error('backup_fails_delay', 'Backup fails delay must be between 60 and 3600 seconds.')
+ return cleaned_data
+
+
+class MessageChannelForm(forms.ModelForm):
+ class Meta:
+ model = MessageChannel
+ fields = [
+ 'name', 'enabled', 'channel_type', 'destination', 'token', 'status_change_offline', 'status_change_online',
+ 'backup_fail', 'daily_status_report', 'daily_backup_report'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(MessageChannelForm, self).__init__(*args, **kwargs)
+ self.helper = FormHelper()
+ self.fields['enabled'].label = 'Channel Enabled'
+ self.helper.layout = Layout(
+ Fieldset(
+ 'Message Channel',
+ ),
+ Div(
+ Div(Field('name'), css_class='col-md-6'),
+ Div(Field('channel_type'), css_class='col-md-6'),
+ css_class='row'),
+ Div(
+ Div(Field('destination'), css_class='col-md-6'),
+ Div(Field('token'), css_class='col-md-6'),
+ css_class='row'),
+
+ Div(
+ Div(
+ Div(HTML('
Notification Settings
'), css_class='col-md-12'),
+
+ Div(Field('status_change_offline'), css_class='col-md-6'),
+ Div(Field('status_change_online'), css_class='col-md-6'),
+ Div(Field('daily_status_report'), css_class='col-md-6'),
+ Div(Field('daily_backup_report'), css_class='col-md-6'),
+ Div(Field('backup_fail'), css_class='col-md-6'),
+ Div(Field('enabled'), css_class='col-md-6'),
+ css_class='row'),
+ Div(
+ Submit('submit', 'Save', css_class='btn btn-success'),
+ HTML(' Back '),
+ css_class='col-md-12'
+ ),
+ css_class='row')
+ )
+
+ def clean(self):
+ cleaned_data = super().clean()
+ name = cleaned_data.get('name')
+ enabled = cleaned_data.get('enabled')
+
+ destination = cleaned_data.get('destination')
+ if destination is not None and len(destination) > 100:
+ self.add_error('destination', 'Destination must be less than 100 characters.')
+
+ token = cleaned_data.get('token')
+ if token is not None and len(token) > 100:
+ self.add_error('token', 'Token must be less than 100 characters.')
+
+ channel_type = cleaned_data.get('channel_type')
+ if channel_type == 'ntfy':
+ self.add_error('channel_type', 'This channel type is not supported. Support will be added soon.')
+
+ test_message = 'Test message from RouterFleet'
+ remote_error = 'No error message received'
+
+ if channel_type == 'callmebot' and enabled:
+ if not token or not destination:
+ raise forms.ValidationError('CallMeBot requires a token and destination.')
+
+ message = requests.get(f'https://api.callmebot.com/whatsapp.php?phone={destination}&text={test_message}&apikey={token}')
+ if message.status_code != 200:
+ if message.text:
+ remote_error = message.text[:200]
+ raise forms.ValidationError(f'Test message failed. CallMeBot API status code {message.status_code}. Error: {remote_error}')
+
+ elif channel_type == 'telegram':
+ if not token or not destination:
+ raise forms.ValidationError('Telegram requires a token and destination.')
+ message = requests.get(f'https://api.telegram.org/bot{token}/sendMessage?chat_id={destination}&text={test_message}')
+ if message.status_code != 200:
+ if message.text:
+ remote_error = message.text[:200]
+ raise forms.ValidationError(f'Test message failed. Telegram API status code {message.status_code}. Error: {remote_error}')
+
+ return cleaned_data
diff --git a/message_center/migrations/0002_alter_messagechannel_channel_type.py b/message_center/migrations/0002_alter_messagechannel_channel_type.py
new file mode 100644
index 0000000..ef6cd8c
--- /dev/null
+++ b/message_center/migrations/0002_alter_messagechannel_channel_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-04-16 17:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('message_center', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='messagechannel',
+ name='channel_type',
+ field=models.CharField(choices=[('callmebot', 'CallMeBot (WhatsApp)'), ('telegram', 'Telegram')], max_length=100),
+ ),
+ ]
diff --git a/message_center/models.py b/message_center/models.py
index f008f4d..67022a2 100644
--- a/message_center/models.py
+++ b/message_center/models.py
@@ -19,7 +19,7 @@ class MessageChannel(models.Model):
enabled = models.BooleanField(default=True)
channel_type = models.CharField(
max_length=100, choices=(
- ('callmebot', 'CallMeBot'), ('ntfy', 'ntfy'), ('telegram', 'telegram'),
+ ('callmebot', 'CallMeBot (WhatsApp)'), ('telegram', 'Telegram'),
)
)
destination = models.CharField(max_length=100, blank=True, null=True)
diff --git a/message_center/views.py b/message_center/views.py
index 91ea44a..12a656e 100644
--- a/message_center/views.py
+++ b/message_center/views.py
@@ -1,3 +1,66 @@
-from django.shortcuts import render
+from django.shortcuts import render, redirect
+from django.contrib import messages
+from user_manager.models import UserAcl
+from .forms import MessageSettingsForm, MessageChannelForm
+from .models import Notification, MessageChannel, Message, MessageSettings
+from django.contrib.auth.decorators import login_required
-# Create your views here.
+
+@login_required()
+def view_message_channel_list(request):
+ if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=20).exists():
+ return render(request, 'access_denied.html', {'page_title': 'Access Denied'})
+ message_settings, _ = MessageSettings.objects.get_or_create(name='message_settings')
+ message_channels = MessageChannel.objects.all()
+ context = {
+ 'message_settings': message_settings,
+ 'message_channels': message_channels,
+ }
+ return render(request, 'message_center/message_channel_list.html', context=context)
+
+
+@login_required()
+def view_manage_message_settings(request):
+ if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=40).exists():
+ return render(request, 'access_denied.html', {'page_title': 'Access Denied'})
+ message_settings, _ = MessageSettings.objects.get_or_create(name='message_settings')
+ form = MessageSettingsForm(request.POST or None, instance=message_settings)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Message Settings saved successfully')
+ return redirect('/message_center/channel_list/')
+ context = {
+ 'message_settings': message_settings,
+ 'form': form,
+ }
+ return render(request, 'generic_form.html', context=context)
+
+
+@login_required()
+def view_manage_message_channel(request):
+ if not UserAcl.objects.filter(user=request.user).filter(user_level__gte=40).exists():
+ return render(request, 'access_denied.html', {'page_title': 'Access Denied'})
+ message_settings, _ = MessageSettings.objects.get_or_create(name='message_settings')
+ if request.GET.get('uuid'):
+ message_channel = MessageChannel.objects.get(uuid=request.GET.get('uuid'))
+ if request.GET.get('action') == 'delete':
+ if request.GET.get('confirmation') == 'delete':
+ message_channel.delete()
+ messages.success(request, 'Message Channel deleted successfully')
+ return redirect('/message_center/channel_list/')
+ else:
+ messages.warning(request, 'Message Channel not deleted|Invalid confirmation')
+ return redirect('/message_center/channel_list/')
+ else:
+ message_channel = None
+
+ form = MessageChannelForm(request.POST or None, instance=message_channel)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Message Channel saved successfully')
+ return redirect('/message_center/channel_list/')
+ context = {
+ 'message_settings': message_settings,
+ 'form': form,
+ }
+ return render(request, 'generic_form.html', context=context)
diff --git a/routerfleet/urls.py b/routerfleet/urls.py
index b74048d..c00c9ba 100644
--- a/routerfleet/urls.py
+++ b/routerfleet/urls.py
@@ -9,6 +9,7 @@ from backup.views import view_backup_profile_list, view_manage_backup_profile, v
from monitoring.views import view_export_router_list, view_update_router_status, view_router_config_timestamp, view_router_last_status_change
from backup_data.views import view_generate_backup_schedule, view_create_backup_tasks, view_perform_backup_tasks, view_housekeeping
from routerfleet_tools.views import cron_check_updates
+from message_center.views import view_message_channel_list, view_manage_message_settings, view_manage_message_channel
urlpatterns = [
@@ -48,5 +49,8 @@ urlpatterns = [
path('cron/check_updates/', cron_check_updates, name='check_updates'),
path('wireguard_webadmin/', view_wireguard_webadmin_launcher, name='wireguard_webadmin_launcher'),
path('wireguard_webadmin/manage/', view_manage_wireguard_integration, name='manage_wireguard_integration'),
- path('wireguard_webadmin/launch/', view_launch_wireguard_webadmin, name='launch_wireguard_webadmin')
+ path('wireguard_webadmin/launch/', view_launch_wireguard_webadmin, name='launch_wireguard_webadmin'),
+ path('message_center/channel_list/', view_message_channel_list, name='message_channel_list'),
+ path('message_center/manage_settings/', view_manage_message_settings, name='manage_message_settings'),
+ path('message_center/manage_channel/', view_manage_message_channel, name='manage_message_channel'),
]
diff --git a/templates/base.html b/templates/base.html
index 25333e9..cdecf1e 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -143,6 +143,15 @@
+
+
+
+
+
+ Message Center
+
+
+
diff --git a/templates/message_center/message_channel_list.html b/templates/message_center/message_channel_list.html
new file mode 100644
index 0000000..e918ac2
--- /dev/null
+++ b/templates/message_center/message_channel_list.html
@@ -0,0 +1,100 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+ Name |
+ Enabled |
+ Channel |
+ Online |
+ Offline |
+ Backup |
+ Status Report |
+ Backup Report |
+
+
+
+
+ {% for channel in message_channels %}
+
+
+
+ {{ channel.name }}
+
+
+ |
+
+ {% if channel.enabled %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+ {{ channel.get_channel_type_display }} |
+
+ {% if channel.status_change_online %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+
+ {% if channel.status_change_offline %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+
+ {% if channel.backup_fail %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+
+ {% if channel.daily_status_report %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+
+ {% if channel.daily_backup_report %}
+ enabled
+ {% else %}
+ disabled
+ {% endif %}
+ |
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}