package NetUseAdminWeb; # NetUseMod is a program to moderate Usenet posts via web interface. # Copyright (C) 2024 Salahuddin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . use Dancer2; use File::Copy 'move'; use HTML::Entities; our $VERSION = '0.1'; my $post_to_usenet = 1; # toggle for testing my $usenet_host = 'YOUR_USENET_PROVIDER'; my $usenet_port = '119'; # production 536 ssl my $rnews_location = '/usr/bin/rnews'; # set user and password in /etc/news/passwd.nntp # to submit news to your newsgroup provider. my $send_rec_ack = 1; my $noreply_mail = 'no-reply@YOUR_DOMAIN'; my $mail_format = 'YOUR_HOME_DIR/NetUseMod/scripts/mail_format.sh'; my $admin_user = 'XXXXXXXX'; my $admin_pass = 'XXXXXXXX'; # example: /home/username/ my $dir_base = 'YOUR_HOME_DIR'; my $dir_incoming = 'incoming_news'; my $dir_processed = 'processed_news'; my $dir_rejected = 'rejected_news'; sub sendReceiveAck { my ($from, $subject, $description) = @_; system('echo "' . $description . '." | mail -s "Re: ' . $subject . '" -r ' . $noreply_mail . ' \'' . $from . '\''); } sub postToNewsGroup { my $processed_dest_file = shift; system($rnews_location . ' -h ' . $usenet_host . ' -P ' . $usenet_port . ' -S ' . $usenet_host . ' < ' . $processed_dest_file); } sub formatNews { my ($filename, $processed_dest_file) = @_; system('cat ' . $filename . ' | ' . $mail_format . ' > ' . $processed_dest_file); } sub getFileData { my $filename = shift; if ($filename eq '') { return ''; } open(FH, '<', $filename) or die $!; my $file_data = ''; while(){ $file_data .= $_; } close(FH); return $file_data; } hook before => sub { if (!session('user') && request->path !~ m{^/login} ) { forward '/login', { requested_path => request->path }; } }; get '/' => sub { template 'index'; }; sub get_news_list { my $dir_type = shift; my $dirname = $dir_base . $dir_type . "/"; opendir my($dh), $dirname or die "Couldn't open dir '$dirname': $!"; my @files = readdir $dh; closedir $dh; # remve .. . entries. for(my $index = 0; $index <= $#files; $index++){ if ($files[$index] eq '.') { splice(@files, $index, 1); #index value changed last; } } for(my $index = 0; $index <= $#files; $index++){ if ($files[$index] eq '..') { splice(@files, $index, 1); #index value changed last; } } my %file_list; foreach(@files) { my $fh = $dirname . '/' . $_; my $epoch_timestamp = (stat($fh))[8]; my $timestamp = localtime($epoch_timestamp); $file_list{$_} = $timestamp; } return %file_list; } get '/listincomingnews' => sub { my %file_list = get_news_list($dir_incoming); my $listnews = ''; for(keys %file_list){ $listnews .= ""; $listnews .= '' . $_ . ''; $listnews .= '' . $file_list{$_} . ''; # created time $listnews .= 'view'; $listnews .= ""; } template 'listnews', { listnews => $listnews }; }; get '/viewfile' => sub { my $filename_param = query_parameters->get('filename'); my $dir_type = query_parameters->get('dirtype'); my $filename = $dir_base . $dir_type . '/' . $filename_param; my $file_data = ''; $file_data = getFileData($filename); if ($file_data eq '') { template 'generic', { message => "emtpy" }; } $file_data = encode_entities($file_data); $file_data =~ s/\n/
/g; my $accept_reject_html_block = ''; if ($dir_type eq $dir_incoming) { my $accept_html_button = 'Accept'; my $reject_html_button = 'Reject'; $accept_reject_html_block = $accept_html_button . '  ' . $reject_html_button; } template 'viewnews', { file_data => $file_data, accept_reject_html_block => $accept_reject_html_block }; }; get '/listprocessednews' => sub { my %file_list = get_news_list($dir_processed); my $listnews = ''; for(keys %file_list){ $listnews .= ""; $listnews .= '' . $_ . ''; $listnews .= '' . $file_list{$_} . ''; # created time $listnews .= 'view'; $listnews .= ""; } template 'listnews', { listnews => $listnews }; }; get '/listrejectednews' => sub { my %file_list = get_news_list($dir_rejected); my $listnews = ''; for(keys %file_list){ $listnews .= ""; $listnews .= '' . $_ . ''; $listnews .= '' . $file_list{$_} . ''; # created time $listnews .= 'view'; $listnews .= ""; } template 'listnews', { listnews => $listnews }; }; get '/acceptnews' => sub { my $filename_param = query_parameters->get('filename'); my $filename = $dir_base . '/' . $dir_incoming . '/' . $filename_param; my $file_data = ''; $file_data = getFileData($filename); if ($file_data eq '') { template 'generic', { message => "emtpy" }; } $file_data = encode_entities($file_data); $file_data =~ s/\n/
/g; template 'acceptnews', { file_data => $file_data, filename_param => $filename_param }; }; post '/acceptnews' => sub { my $filename_param = body_parameters->get('filename_param'); my $filename = $dir_base . '/' . $dir_incoming . '/' . $filename_param; my $dest_filename = $dir_base . '/' . $dir_processed . '/' . $filename_param; my $processed_dest_file = $dest_filename . '_processed'; formatNews($filename, $processed_dest_file); if ($post_to_usenet eq 1) { postToNewsGroup($processed_dest_file); } move($filename, $dest_filename)or die "The move operation failed: $!"; template 'generic', { message => 'Success' }; }; get '/rejectnews' => sub { my $filename_param = query_parameters->get('filename'); my $filename = $dir_base . '/' . $dir_incoming . '/' . $filename_param; my $file_data = ''; $file_data = getFileData($filename); if ($file_data eq '') { template 'generic', { message => "emtpy" }; } $file_data = encode_entities($file_data); $file_data =~ s/\n/
/g; template 'rejectnews', { file_data => $file_data, filename_param => $filename_param }; }; sub parseFromAndSubject { my $message = shift; my $from = ''; my $subject = ''; my @message_array = split(/\n/, $message); foreach(@message_array) { # first empty line if(/^$/) { last; } if( /^From: / ) { $from = $_; $from=~ s/^From: //i; } elsif( /^Subject: / ) { $subject = $_; $subject =~ s/^Subject: //i; } } # trim $from =~ s/^\s+|\s+$//g; $subject =~ s/^\s+|\s+$//g; return ($from, $subject); } post '/rejectnews' => sub { my $submitbutton = body_parameters->get('submitbutton'); my $filename_param = body_parameters->get('filename_param'); my $reject_reason = body_parameters->get('reject_reason'); my $filename = $dir_base . '/' . $dir_incoming . '/' . $filename_param; my $dest_filename = $dir_base . '/' . $dir_rejected . '/' . $filename_param; if (index($submitbutton, 'Send reject notification') != -1) { # TODO: send notification if ($send_rec_ack eq 1) { open(FH, '<', $filename) or die $!; my $file_data = ''; while(){ # first empty line if(/^$/) { last; } $file_data .= $_; } close(FH); my ($from, $subject) = parseFromAndSubject($file_data); # TODO: check return value sendReceiveAck($from, $subject, $reject_reason); } } # add error in error variable move($filename, $dest_filename)or die "The move operation failed: $!"; my $message = 'Success'; template 'generic', { message => $message }; }; get '/login' => sub { if (session('user')) { redirect '/'; } else { template 'login', { path => param('requested_path') }; } }; post '/login' => sub { my $user_value = body_parameters->get('user'); my $pass_value = body_parameters->get('pass'); my $error_message = ''; if($user_value eq $admin_user && $pass_value eq $admin_pass) { session user => $user_value; redirect body_parameters->get('path') || '/'; } else { $error_message = 'Invalid login/password.'; } if($error_message ne '') { return template 'generic', { message => $error_message}; } }; get '/logout' => sub { app->destroy_session; redirect '/'; }; true;