diff --git a/.idea/templateLanguages.xml b/.idea/templateLanguages.xml new file mode 100644 index 0000000..7302b77 --- /dev/null +++ b/.idea/templateLanguages.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Makefile.PL b/Makefile.PL index 9260e20..7f281c2 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -6,9 +6,10 @@ use ExtUtils::MakeMaker; WriteMakefile( VERSION => '0.01', PREREQ_PM => { - 'Mojolicious' => '9.27', - 'Email::MIME' => '1.952', - 'Email::Sender::Simple' => '2.500' + 'Mojolicious' => '9.27', + 'Email::MIME' => '1.952', + 'Email::Sender::Simple' => '2.500', + 'File::ChangeNotify' => '0.31', }, test => {TESTS => 't/*.t'} ); diff --git a/lib/unix_dog.pm b/lib/unix_dog.pm index 2e621fd..975d396 100644 --- a/lib/unix_dog.pm +++ b/lib/unix_dog.pm @@ -32,6 +32,10 @@ sub startup ($self) { $r->post('/account')->to('Account#update_account'); $r->get('/users')->to('Main#user_pages'); + + $r->get('/announcements')->to('Main#announcements'); + $r->get('/announcements.rss')->to('Main#announcement_feed'); + $r->get('/announcement/#filename')->to('Main#get_announcement'); } 1; diff --git a/lib/unix_dog/Controller/Main.pm b/lib/unix_dog/Controller/Main.pm index c87a95c..5a1dee8 100644 --- a/lib/unix_dog/Controller/Main.pm +++ b/lib/unix_dog/Controller/Main.pm @@ -5,6 +5,11 @@ use experimental 'signatures'; use Mojo::Base 'Mojolicious::Controller', -signatures; use Net::LDAPS; +use File::ChangeNotify; +use File::stat; +use File::Basename; +use Time::Piece; +use Scalar::Util qw(looks_like_number); sub index ($self) { $self->render(); @@ -25,6 +30,9 @@ Mojo::IOLoop->recurring(60 => sub { $cache->set(users => []); }); +my $acache = Mojo::Cache->new(max_keys => 1); +my $filewatcher; + sub user_pages ($self) { my $config = $self->config; my $connStr = $config->{'ldap'}->{'uri'}; @@ -60,4 +68,166 @@ sub user_pages ($self) { }); } + +sub announcements ($self) { + if (!defined $acache->get('announcements')) { + setup_filewatch($self->config->{'announce'}->{'path'}); + } else { + check_files(); + } + + my %announcements = %{$acache->get('announcements')}; + my $page = $self->param('page'); + if (!defined $page) { + $page = 0; + } + + if (!looks_like_number($page) || $page < 0) { + return $self->reply->not_found; + } + + + my $total_items = scalar(keys %announcements); + my $items_per_page = 5; + + my $start_index = $page * $items_per_page; + my $end_index = $start_index + $items_per_page-1; + + if ($start_index >= $total_items) { + return $self->reply->not_found; + } + + my $next_page = -1; + if ($end_index >= $total_items) { + $end_index = $total_items-1; + } elsif ($end_index != $total_items-1){ + $next_page = $page + 1; + } + + + + $self->stash( + announcements => \%announcements, + total_items => $total_items, + items_per_page => $items_per_page, + page => $page, + start_index => $start_index, + end_index => $end_index, + next_page => $next_page, + ); + $self->render(); +} + +sub get_announcement ($self) { + if (!defined $acache->get('announcements')) { + setup_filewatch($self->config->{'announce'}->{'path'}); + } else { + check_files(); + } + + my $announcements = $acache->get('announcements'); + my $filename = $self->param('filename'); + if (!defined $announcements->{$filename}) { + return $self->reply->not_found; + } + my $entry = $announcements->{$filename}; + $self->render(text => $entry->{'raw'}, format => 'txt'); +} + +sub announcement_feed ($self) { + if (!defined $acache->get('announcements')) { + setup_filewatch($self->config->{'announce'}->{'path'}); + } else { + check_files(); + } + + my $announcements = $acache->get('announcements'); + my @chosen = sort (keys %$announcements); + + if (scalar(@chosen) > 5) { + @chosen = @chosen[0..4]; + } + + my @elements; + + for my $key (@chosen) { + push @elements, $announcements->{$key}; + } + + $self->stash(items => \@elements); + $self->render('main/announcements', format => 'rss'); +} + +sub setup_filewatch($path) { + opendir my $dir, $path or die "Could not open directory!"; + my @files = readdir $dir; + closedir $dir; + + my %announcements; + + for my $filename (@files) { + if ($filename eq "." || $filename eq "..") { + next; + } + + my $entry = get_entry($path.$filename); + if (!defined $entry) { + say "Invalid entry at $filename"; + next; + } + + $announcements{$filename} = $entry; + } + $acache->set(announcements => \%announcements); + + $filewatcher = File::ChangeNotify->instantiate_watcher( + directories => [$path], + ); +} + +sub check_files { + for my $event ($filewatcher->new_events) { + if ($event->path =~ /\.txt$/) { + my $filename = basename($event->path); + if ($event->type eq 'create' || $event->type eq 'modify') { + my $entry = get_entry($event->path); + if (!defined $entry) { + say "Invalid entry at $filename"; + delete $acache->get('announcements')->{$filename}; + next; + } + $acache->get('announcements')->{$filename} = get_entry($event->path); + } elsif ($event->type eq 'delete') { + delete $acache->get('announcements')->{$filename}; + } + } + } +} + +sub get_entry($fullpath) { + open FH, '<', $fullpath or die "Could not open file! $!"; + read FH, my $announce_content, -s FH; + close FH; + my $filestat = stat($fullpath); + my $announce_text; + my $announce_title; + if ($announce_content =~ /-----BEGIN PGP SIGNED MESSAGE-----\nHash: \w+\n\n(.+?)\n\n(.*)\n-----BEGIN PGP SIGNATURE-----/s) { + $announce_title = $1; + $announce_text = $2; + } + + if (!defined $announce_title || !defined $announce_content || length $announce_title == 0 || length $announce_content == 0) { + return undef; + } + + + return { + raw => $announce_content, + text => $announce_text, + date => $filestat->mtime, + filename => basename($fullpath), + title => $announce_title, + }; +} + 1; diff --git a/public/css/main.css b/public/css/main.css index 0cd9af1..437b9c4 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -140,6 +140,10 @@ textarea { padding: 10px; } +pre { + white-space: pre-wrap; +} + #header-content a:link { text-decoration: none; color: black; diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 2c27912..c4ce056 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -3,8 +3,9 @@ <%= title %> - - + + + @@ -19,11 +20,12 @@
- UNIX.dog + UNIX.dog

UNIX.dog

diff --git a/templates/main/announcements.html.ep b/templates/main/announcements.html.ep new file mode 100644 index 0000000..6567eff --- /dev/null +++ b/templates/main/announcements.html.ep @@ -0,0 +1,31 @@ +% layout 'default'; +% title 'UNIX.dog Announcements'; +% use Time::Piece; +

+ Announcements +

+

+ These announcements are also avaiable over RSS. Just add .rss to the path here. +
+ Also, all announcements are signed with the following PGP key: +

+
67A3 9437 8618 B72E B4D5  CC69 B0EB E117 49B9 9367 alpha@unix.dog
+

+ so you can verify their authenticity. When a new announcement is out, you should + also recieve an email if you're registered. +

+<% my @sorted = sort (keys %$announcements); %> +<% foreach my $key (@sorted[$start_index..$end_index]) { %> + <% my $value = $announcements->{$key}; %> +
+

<%= $value->{"title"} %>

+

+ Posted <%= localtime($value->{"date"})->strftime() %>. + ">Download PGP signature. +

+

<%= $value->{"text"} %>
+
+<% } %> +<% if ($next_page != -1) { %> + Next page... +<% } %> diff --git a/templates/main/announcements.rss.ep b/templates/main/announcements.rss.ep new file mode 100644 index 0000000..6c5eb74 --- /dev/null +++ b/templates/main/announcements.rss.ep @@ -0,0 +1,26 @@ +% use Time::Piece; + + + + UNIX.dog Announcements + https://unix.dog/announcements + + UNIX.dog Announcements + en-us + + UNIX.dog Announcements + https://unix.dog/favicon.ico + https://unix.dog/announcements + + % foreach my $item (@$items) { + + <![CDATA[<%= $item->{'title'} %>]]> + https://unix.dog/announcement/<%= $item->{'filename'} %> + https://unix.dog/announcement/<%= $item->{'filename'} %> + {'text'} %>]]> + Alpha + <%= localtime($item->{'date'})->strftime("%a, %d %b %Y %H:%M:%S %z") %> + + % } + + diff --git a/unix_dog.default.yml b/unix_dog.default.yml index b4bfbb9..afa0c72 100644 --- a/unix_dog.default.yml +++ b/unix_dog.default.yml @@ -12,3 +12,7 @@ ldap: uri: 'ldaps://127.0.0.1' bindDN: 'here' password: 'here' + +# Leading slash +announce: + path: '/srv/announce/'