App-phoebe

 view release on metacpan or  search on metacpan

lib/App/Phoebe/Oddmuse.pm  view on Meta::CPAN

# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.

=encoding utf8

=head1 NAME

App::Phoebe::Oddmuse - act as a Gemini proxy for an Oddmuse wiki

=head1 DESCRIPTION

This extension allows you to serve files from an Oddmuse wiki instead of a real
Phoebe wiki directory.

The tricky part is that most Oddmuse wikis don't use Gemini markup (“gemtext”)
and therefore care is required. The extension tries to transmogrify typical
Oddmuse markup (based on my own wikis) to Gemini.

Here's one way to configure it. I use Apache as my proxy server and have
multiple Oddmuse wikis running on the same machine, each only serving
C<localhost>. I need to recreate some of the Apache configuration, here.

    package App::Phoebe::Oddmuse;

    our %oddmuse_wikis = (
      "alexschroeder.ch" => "http://localhost:4023/wiki",
      "communitywiki.org" => "http://localhost:4019/wiki",
      "emacswiki.org" => "http://localhost:4002/wiki",
      "campaignwiki.org" => "http://localhost:4004/wiki", );

    our %oddmuse_wiki_names = (
      "alexschroeder.ch" => "Alex Schroeder",
      "communitywiki.org" => "Community Wiki",
      "emacswiki.org" => "Emacs Wiki",
      "campaignwiki.org" => "Campaign Wiki", );

    our %oddmuse_wiki_dirs = (
      "alexschroeder.ch" => "/home/alex/alexschroeder",
      "communitywiki.org" => "/home/alex/communitywiki",
      "emacswiki.org" => "/home/alex/emacswiki",
      "campaignwiki.org" => "/home/alex/campaignwiki", );

    our %oddmuse_wiki_links = (
      "communitywiki.org" => 1,
      "campaignwiki.org" => 1, );

    use App::Phoebe::Oddmuse;

=cut

package App::Phoebe::Oddmuse;
use App::Phoebe qw(@request_handlers @extensions @main_menu $server $log $full_url_regex
		   success result reserved_regex port gemini_link modified changes diff
		   colourize quote_html bogus_hash print_link);
use Mojo::UserAgent;
use Modern::Perl;
use MIME::Base64;
use URI::Escape;
use List::Util qw(uniq);
use Encode qw(encode_utf8 decode_utf8);
use DateTime::Format::ISO8601;
use utf8; # the source contains UTF-8 encoded strings
no warnings 'redefine';

# Oddmuse Wiki

our %oddmuse_wikis = (
  "alexschroeder.ch" => "http://localhost:4023/wiki",
  "communitywiki.org" => "http://localhost:4019/wiki",
  "emacswiki.org" => "http://localhost:4002/wiki" );

our %oddmuse_wiki_names = (
  "alexschroeder.ch" => "Alex Schroeder",
  "communitywiki.org" => "Community Wiki",
  "emacswiki.org" => "Emacs Wiki" );

our %oddmuse_wiki_dirs = (
  "alexschroeder.ch" => "/home/alex/alexschroeder",
  "communitywiki.org" => "/home/alex/communitywiki",
  "emacswiki.org" => "/home/alex/emacswiki" );

# The Oddmuse wiki uses WikiLinks
our %oddmuse_wiki_links = ("communitywiki.org" => 1);

# The Oddmuse wiki uses a different token as the answer to a security question
# (i.e. not the Phoebe server token). This only works if the Oddmuse wiki has
# just one security question (or accepts the same answer for all questions).
our %oddmuse_wiki_tokens = (
  "emacswiki.org" => "emacs" );

our $oddmuse_namespace_regex = '[\p{Uppercase}\d][\w_  ]*';

*oddmuse_old_space_regex = \&App::Phoebe::space_regex;
*App::Phoebe::space_regex = \&oddmuse_new_space_regex;

sub oddmuse_new_space_regex {
  my $spaces = oddmuse_old_space_regex();
  return "$spaces|$oddmuse_namespace_regex" if $spaces;
  return $oddmuse_namespace_regex;
}

*oddmuse_old_space = \&App::Phoebe::space;
*App::Phoebe::space = \&oddmuse_new_space;

sub oddmuse_new_space {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  if (grep { $_ eq $host } keys %oddmuse_wikis) {
    # Let Oddmuse handle namespaces
    return $space;
  }
  return oddmuse_old_space($stream, $host, $space);
}

*oddmuse_old_save_page = \&App::Phoebe::save_page;
*App::Phoebe::save_page = \&oddmuse_new_save_page;

sub oddmuse_new_save_page {
  my $stream = shift;
  my $host = shift;

lib/App/Phoebe/Oddmuse.pm  view on Meta::CPAN

    $url .= ";match=" . uri_escape($re) if $re;
    return map { s/_/ /g; $_ } split(/\n/, oddmuse_get_raw($stream, $url));
  }
  return oddmuse_pages_old($stream, $host, $space, $re);
}

# this is required when combining gopher with oddmuse!
*oddmuse_search_old = \&App::Phoebe::search;
*App::Phoebe::search = \&oddmuse_search_new;

sub oddmuse_search_new {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $re = shift;
  if (exists $oddmuse_wikis{$host}) {
    my $url = "$oddmuse_wikis{$host}?raw=1";
    $url .= ";ns=$space" if $space;
    $url .= ";context=0;search=" . uri_escape($re) if $re;
    return map { s/_/ /g; $_ } split(/\n/, oddmuse_get_raw($stream, $url));
  }
  return oddmuse_search_old($stream, $host, $space, $re, @_);
}

sub oddmuse_serve_changes {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $n = shift;
  my $style = shift;
  my $all = shift;
  $log->info("Serving changes for $n days");
  success($stream);
  $stream->write("# Changes\n");
  if (not $style) { print_link($stream, $host, $space, "Colour changes", "do/changes/$n/colour") }
  elsif ($style eq "colour") { print_link($stream, $host, $space, "Fancy changes", "do/changes/$n/fancy") }
  elsif ($style eq "fancy") { print_link($stream, $host, $space, "Normal changes", "do/changes/$n") }
  print_link($stream, $host, $space, "Atom Feed", "do/atom");
  print_link($stream, $host, $space, "RSS Feed", "do/rss");
  my $url = "$oddmuse_wikis{$host}?raw=1;action=rc;all=1;showedit=1;days=$n";
  if ($space) {
    $url .= ";ns=$space";
  } if (not $all) {
    $url .= ";local=1";
  }
  my $page = oddmuse_get_raw($stream, $url) // return;
  my @entries = split(/\n\n+/, $page);
  shift @entries; # skip head
  my $log;
  foreach my $entry (@entries) {
    my $data = parse_data($entry);
    # namespaces
    my $ns = $space;
    my $title = $data->{title};
    if (not $ns and $title =~ /:/) {
      ($ns, $title) = split(/:/, $title);
    }
    # timestamp from 2020-07-22T20:59Z back to a number
    my $ts = $data->{"last-modified"};
    $ts =~ s/Z/:00Z/; # apparently seconds are mandatory?
    $ts = DateTime::Format::ISO8601->parse_datetime($ts)->epoch();
    # author from some string back to a bogus hash: [0-7]{4}
    my $author = $data->{generator};
    $author = bogus_hash($stream, encode_utf8($author)) unless $author =~ /^[0-7]{4}$/;
    push(@$log, [
	   $ts,
	   free_to_normal($title),
	   $data->{revision},
	   $author,
	   $host,
	   $ns,
	   1]);   # show space
  }
  # taking the head of the @$log to get new log entries
  $stream->write("Showing up to $n days.\n");
  changes($stream,
    scalar(@$log), # just show them all
    sub { $stream->write("## " . shift . "\n") },
    sub { $stream->write(encode_utf8 shift . " by " . colourize($stream, shift, $style) . "\n") },
    sub {
      my ($host, $space, $title, $id) = @_;
      $title =~ s/_/ /g;
      print_link($stream, $host, $space, $title, $id) },
    sub { $stream->write(encode_utf8 join("\n", @_, "")) },
    sub { @{shift(@$log) } if @$log },
    sub { 1 }, # show a diff link, always
      );
  # there is always more...
  $stream->write("\n");
  print_link($stream, $host, $space, "More...", "do/changes/" . 2 * $n . ($style ? "/$style" : ""));
}

sub oddmuse_serve_history {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $id = shift;
  my $style = shift;
  success($stream);
  $log->info("Serve history for $id");
  $stream->write("# Page history for " . normal_to_free($id) . "\n");
  if (not $style) { print_link($stream, $host, $space, "Colour history", "history/$id/colour") }
  elsif ($style eq "colour") { print_link($stream, $host, $space, "Fancy history", "history/$id/fancy") }
  elsif ($style eq "fancy") { print_link($stream, $host, $space, "Normal history", "history/$id") }
  my $url = "$oddmuse_wikis{$host}?raw=1;action=history;id=" . uri_escape_utf8($id);
  $url .= ";ns=$space" if $space;
  my $page = oddmuse_get_raw($stream, $url) // return;
  my @entries = split(/\n\n+/, $page);
  shift @entries; # skip head
  my $log;
  foreach my $entry (@entries) {
    my $data = parse_data($entry);
    # timestamp from 2020-07-22T20:59Z back to a number
    my $ts = $data->{"last-modified"};
    $ts =~ s/Z/:00Z/; # apparently seconds are mandatory?
    $ts = DateTime::Format::ISO8601->parse_datetime($ts)->epoch();
    # author from some string back to a bogus hash: [0-7]{4}
    my $author = $data->{generator};
    $author = bogus_hash($stream, encode_utf8($author)) unless $author =~ /^[0-7]{4}$/;
    push(@$log, [
	   $ts,
	   free_to_normal($data->{title}),
	   $data->{revision},
	   $author,
	   $host,
	   $space, # space
	   0]);   # show space
  }
  # taking the head of the @$log to get new log entries
  changes($stream,
    scalar(@$log), # just show them all
    sub { $stream->write("## " . shift . "\n") },
    sub { $stream->write(shift . " by " . colourize($stream, shift, $style) . "\n") },
    sub {
      my ($host, $space, $title, $id) = @_;
      $title =~ s/_/ /g;
      print_link($stream, $host, $space, $title, $id) },
    sub { $stream->write(join("\n", @_, "")) },
    sub { @{shift(@$log) } if @$log },
    sub { 1 }, # show a diff link, always
      );
}

sub oddmuse_serve_diff {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $id = shift;
  my $revision = shift;
  my $style = shift;
  $log->info("Serving the diff of $id");
  success($stream);
  $stream->write("# Differences for " . normal_to_free($id) . "\n");
  if (not $style) { print_link($stream, $host, $space, "Colour diff", "diff/$id/$revision/colour") }
  else { print_link($stream, $host, $space, "Normal diff", "diff/$id/$revision") }
  $stream->write("Showing the differences between revision $revision and the current revision.\n");
  my $url = $oddmuse_wikis{$host} . ($space ? "/$space" : "") . "/raw/" . uri_escape_utf8($id);
  my $new = oddmuse_get_raw($stream, $url);
  $url .= "?revision=$revision" if $revision;
  my $old = oddmuse_get_raw($stream, $url);
  if (not $style) {
    diff($old, $new,
	 sub { $stream->write(encode_utf8 "$_\n") for @_ },
	 sub { $stream->write(encode_utf8 "> $_\n") for map { $_||"⏎" } @_ },
	 sub { $stream->write(encode_utf8 "> $_\n") for map { $_||"⏎" } @_ },
	 sub { "ï½¢$_[0]ï½£" });
  } else {
    diff($old, $new,
	 sub { $stream->write(encode_utf8 "$_\n") for @_ },
	 sub { $stream->write(encode_utf8 "> \033[31m$_\033[0m\n") for map { $_||"⏎" } @_ },
	 sub { $stream->write(encode_utf8 "> \033[32m$_\033[0m\n") for map { $_||"⏎" } @_ },
	 sub { "\033[1m$_[0]\033[22m" });
  }
}

sub oddmuse_serve_match {

lib/App/Phoebe/Oddmuse.pm  view on Meta::CPAN

  my @entries = split(/\n\n+/, $page);
  shift @entries; # skip head
  foreach my $entry (@entries) {
    my $data = parse_data($entry);
    my $id = $data->{title};
    print_link($stream, $host, $space, normal_to_free($id), "page/$id");
  }
}

sub oddmuse_serve_rss {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $action = shift;
  my $scheme = 'gemini';
  my $port = port($stream);
  $log->info("Serving Gemini RSS");
  success($stream, "application/rss+xml");
  $stream->write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
  $stream->write("<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n");
  my $url = "$oddmuse_wikis{$host}?action=$action;raw=1;full=1";
  if ($space) {
    $url .= ";ns=$space";
  }
  my $page = oddmuse_get_raw($stream, $url) // return;
  my @entries = split(/\n\n+/, $page);
  my $entry = shift @entries;
  my $data = parse_data($entry);
  $stream->write("<channel>\n");
  $stream->write(encode_utf8 "<title>" . quote_html($data->{title}) . "</title>\n");
  $stream->write(encode_utf8 "<description>" . quote_html($data->{description}) . "</description>\n");
  $stream->write("<link>$scheme://$host:$port/</link>\n");
  $stream->write("<atom:link rel=\"self\" type=\"application/rss+xml\" href=\"$scheme://$host:$port/do/rss\" />\n");
  $stream->write("<generator>Phoebe + Config</generator>\n");
  $stream->write("<docs>http://blogs.law.harvard.edu/tech/rss</docs>\n");
  my $dir = $oddmuse_wiki_dirs{$host};
  my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(modified("$dir/pageidx"));
  $stream->write("<updated>"
      . sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec)
      . "</updated>\n");
  while (@entries) {
    $data = parse_data(shift(@entries));
    $stream->write("<item>\n");
    # namespaces
    my $ns;
    my $title = $data->{title};
    if ($title =~ /:/) {
      ($ns, $title) = split(/:/, $title);
    }
    $stream->write(encode_utf8 "<title>" . quote_html($data->{title}) . "</title>\n");
    my $link = "gemini://$host:$port/" . ($ns ? "$ns/" : "") . "page/" . uri_escape_utf8(free_to_normal($title));
    $stream->write("<link>$link</link>\n");
    $stream->write("<guid>$link</guid>\n");
    $link = "gemini://$host:$port/" . ($ns ? "$ns/" : "") . "page/Comments_on_" . uri_escape_utf8(free_to_normal($title));
    $stream->write("<comments>$link</comments>\n");
    my $summary = quote_html(oddmuse_gemini_text($stream, $host, $space, $data->{description}));
    $stream->write(encode_utf8 "<description>$summary</description>\n") if $summary;
    # timestamp from 2020-07-22T20:59Z back to a number
    my $ts = $data->{"last-modified"};
    $ts =~ s/Z/:00Z/; # apparently seconds are mandatory?
    $ts = DateTime::Format::ISO8601->parse_datetime($ts)->epoch();
    my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($ts); # Sat, 07 Sep 2002 00:00:01 GMT
    $stream->write("<pubDate>"
	. sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
		  qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon], $year + 1900, $hour, $min, $sec)
	. "</pubDate>\n");
    $stream->write("</item>\n");
  };
  $stream->write("</channel>\n");
  $stream->write("</rss>\n");
}

sub oddmuse_serve_atom {
  my $stream = shift;
  my $host = shift;
  my $space = shift;
  my $action = shift;
  my $port = port($stream);
  $log->info("Serving Gemini Atom");
  success($stream, "application/atom+xml");
  $stream->write(qq{<?xml version="1.0" encoding="UTF-8"?>\n});
  $stream->write("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n");
  $stream->write("<link href=\"gemini:/$host:$port/\"/>\n");
  $stream->write("<link rel=\"self\" type=\"application/atom+xml\" href=\"gemini://$host:$port/do/atom\"/>\n");
  $stream->write("<id>gemini:/$host:$port/do/atom</id>\n");
  my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(modified("$oddmuse_wiki_dirs{$host}/pageidx"));
  $stream->write("<updated>"
      . sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", $year + 1900, $mon + 1, $mday, $hour, $min, $sec)
      . "</updated>\n");
  $stream->write("<generator uri=\"gemini://$host:$port/\" version=\"1.0\">Gemini Wiki + Config</generator>\n");
  # now get the data and print the entries
  my $url = "$oddmuse_wikis{$host}?action=$action;raw=1;full=1";
  if ($space) {
    $url .= ";ns=$space";
  }
  my $page = oddmuse_get_raw($stream, $url) // return;
  my @entries = split(/\n\n+/, $page);
  my $data = parse_data(shift @entries);
  $stream->write(encode_utf8 "<title>" . quote_html($data->{title}) . "</title>\n");
  while (@entries) {
    $data = parse_data(shift @entries);
    $stream->write("<entry>\n");
    my $name = $data->{title};
    $stream->write(encode_utf8 "<title>$name</title>\n");
    my $link = "gemini://$host:$port/page/" . uri_escape_utf8(free_to_normal($name));
    $stream->write("<link href=\"$link\"/>\n");
    $stream->write("<id>$link</id>\n");
    my $summary = quote_html(oddmuse_gemini_text($stream, $host, $space, $data->{description}));
    $stream->write(encode_utf8 "<content type=\"text\">$summary</content>\n") if $summary;
    $stream->write("<updated>$data->{'last-modified'}</updated>\n");
    $stream->write("</entry>\n");
  };
  $stream->write("</feed>\n");
}

sub oddmuse_serve_config {
  my $stream = shift;
  my $file = shift;
  $log->info("Serving Config");
  my $dir = $server->{wiki_dir};
  my @config;



( run in 0.608 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )