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 @@

+ +