App-Phoebe
view release on metacpan or search on metacpan
lib/App/Phoebe/Ijirait.pm view on Meta::CPAN
$stream->on(error => sub { my ($stream, $err) = @_; logout($stream, $p, $err) });
push(@streamers, { stream => $stream, person => $p });
success($stream);
$stream->write(encode_utf8 "# Streaming $p->{name}\n");
$stream->write(encode_utf8 "Make sure you connect to game using a different client "
. "(with the same client certificate!) in order to play $p->{name}.\n");
$stream->write(encode_utf8 "=> /play/ijirait Play $p->{name}.");
# don't close the stream!
} else {
$stream->close_gracefully();
}
} else {
result($stream, "59", "Don't know how to handle $url");
$stream->close_gracefully();
}
}
sub logout {
my ($stream, $p, $msg) = @_;
$log->debug("Disconnected $p->{name}: $msg");
@streamers = grep { $_->{stream} ne $stream and $_->{person} ne $p } @streamers;
}
# run every minute and print a timestamp every 5 minutes
Mojo::IOLoop->recurring(60 => sub {
my $loop = shift;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
return unless $min % 5 == 0;
return unless @streamers > 0;
$log->debug("Ijirait streamer ping");
my $ts = sprintf("%02d:%02d UTC\n", $hour, $min);
for (@streamers) {
$_->{stream}->write($ts);
}});
# notify every streamer in the same room
sub notify {
my ($p, $msg) = @_;
eval {
for my $s (grep { $_->{person}->{location} == $p->{location} } @streamers) {
my $stream = $s->{stream};
next unless $stream;
$stream->write(encode_utf8 $msg);
$stream->write("\n");
}
};
$log->error("Error notifying people of '$msg': $@") if $@;
}
# main loop
push(@extensions, \&main);
sub main {
my $stream = shift;
my $url = shift;
my $port = App::Phoebe::port($stream);
if ($url =~ m!^gemini://(?:$host)(?::$port)?/play/ijirait\.tar\.gz$!) {
export_archive($stream);
}
elsif ($url =~ m!^gemini://(?:$host)(?::$port)?/play/ijirait(?:/([a-z]+))?(?:\?(.*))?!) {
my $command = ($1 || "look") . ($2 ? " " . decode_utf8 uri_unescape($2) : "");
$log->debug("Handling $url - $command");
# some commands require no client certificate (and no person argument!)
my $routine = $ijrait_commands_without_cert->{$command};
if ($routine) {
$log->debug("Running $command");
$routine->($stream);
return 1;
}
# regular commands
my $p = login($stream);
if ($p) {
type($stream, $p, $command);
}
return 1;
}
return 0;
}
sub login {
my ($stream) = @_;
# you need a client certificate
my $fingerprint = $stream->handle->get_fingerprint();
if (!$fingerprint) {
$log->info("Requested client certificate");
result($stream, "60", "You need a client certificate to play");
return;
}
# find the right person
my $p = first { $_->{fingerprint} eq $fingerprint} @{$data->{people}};
if (!$p) {
# create a new person if we can't find one
$log->info("New client certificate $fingerprint");
$p = new_person($fingerprint);
} else {
$log->info("Successfully identified client certificate: " . $p->{name});
}
return $p;
}
sub new_person {
my $fingerprint = shift;
my $p = {
id => $data->{next}++,
name => person_name(),
description => "A shape-shifter with red eyes.",
fingerprint => $fingerprint,
location => 2, # The Tent
seen => [],
ts => time,
};
push(@{$data->{people}}, $p);
return $p;
}
sub person_name {
my $digraphs = "..lexegezacebisousesarmaindire.aeratenberalavetiedorquanteisrion";
my $max = length($digraphs);
my $length = 4 + rand(7); # 4-8
my $name = '';
while (length($name) < $length) {
$name .= substr($digraphs, 2*int(rand($max/2)), 2);
}
$name =~ s/\.//g;
return ucfirst $name;
}
sub look {
my ($stream, $p) = @_;
success($stream);
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
$stream->write(encode_utf8 "# " . $room->{name} . "\n");
$stream->write(encode_utf8 $room->{description} . "\n") if $room->{description};
my @things = grep { my $thing = $_;
not $thing->{hidden}
or $thing->{seen}
and any { $_ eq $thing->{seen} } @{$p->{seen}}} @{$room->{things}};
$stream->write("## Things\n") if @things > 0;
for my $thing (@things) {
my $name = uri_escape_utf8 $thing->{short};
$stream->write(encode_utf8 "=> /play/ijirait/examine?$name $thing->{name} ($thing->{short})\n");
}
my @exits = grep { my $exit = $_;
not $exit->{hidden}
or $exit->{seen}
and any { $_ eq $exit->{seen} } @{$p->{seen}} } @{$room->{exits}};
$stream->write("## Exits\n") if @exits > 0;
for my $exit (@exits) {
my $direction = uri_escape_utf8 $exit->{direction};
$stream->write(encode_utf8 "=> /play/ijirait/go?$direction $exit->{name} ($exit->{direction})\n");
}
$stream->write("## People\n"); # there is always at least the observer!
my $n = 0;
my $now = time;
for my $o (@{$data->{people}}) {
next unless $o->{location} == $p->{location};
next if $now - $o->{ts} > 600; # don't show people inactive for 10min or more
$n++;
my $name = uri_escape_utf8 $o->{name};
if ($o->{id} == $p->{id}) {
$stream->write(encode_utf8 "=> /play/ijirait/examine?$name $o->{name} (you)\n");
} else {
$stream->write(encode_utf8 "=> /play/ijirait/examine?$name $o->{name}\n");
}
}
my $title = 0;
for my $word (@{$room->{words}}) {
next if $now - $word->{ts} > 600; # don't show messages older than 10min
$stream->write("## Words\n") unless $title++;
if ($word->{by}) {
my $o = first { $_->{id} == $word->{by} } @{$data->{people}};
$stream->write(encode_utf8 ucfirst timespan($now - $word->{ts})
. ", " . $o->{name} . " said â" . $word->{text} . "â\n");
} elsif ($word->{text}) {
# emotes
$stream->write(encode_utf8 $word->{text} . "\n");
}
}
menu($stream);
}
sub export {
my $stream = shift;
result($stream, "31", "/play/ijirait.tar.gz"); # redirect in order to get a better filename
}
sub export_archive {
my $stream = shift;
success($stream, "application/gzip");
my $tar = Archive::Tar->new;
my $bytes = "# Rooms\n";
$bytes .= encode_utf8 "=> rooms/$_->{id}.gmi $_->{name}\n" for @{$data->{rooms}};
$tar->add_data("ijirait/index.gmi", $bytes);
for my $room (@{$data->{rooms}}) {
$bytes = encode_utf8 "# " . $room->{name} . "\n";
$bytes .= encode_utf8 $room->{description} . "\n" if $room->{description};
my @things = @{$room->{things}};
$bytes .= "## Things\n" if @things > 0;
for my $thing (@things) {
my $name = uri_escape_utf8 $thing->{short};
$bytes .= encode_utf8 "=> ../things/$room->{id}/$name.gmi $thing->{name}\n";
my $bytes2 = encode_utf8 "# $thing->{name}\n";
$bytes2 .= encode_utf8 "$thing->{description}\n";
$bytes2 .= "=> ../../rooms/$room->{id}.gmi Back\n";
$tar->add_data("ijirait/things/$room->{id}/$name.gmi", $bytes2);
}
my @exits = @{$room->{exits}};
$bytes .= "## Exits\n" if @exits > 0;
for my $exit (@exits) {
my $direction = uri_escape_utf8 $exit->{direction};
my $destination = first { $_->{id} == $exit->{destination} } @{$data->{rooms}};
$bytes .= encode_utf8 "=> $destination->{id}.gmi $exit->{name}\n";
}
# We also print rooms without people! We also print inactive people.
my @people = grep { $_->{location} == $room->{id} } @{$data->{people}};
$bytes .= "## People\n" if @people > 0;
for my $o (@people) {
$bytes .= encode_utf8 "=> ../people/$o->{id}.gmi $o->{name}\n";
my $bytes2 = encode_utf8 "# $o->{name}\n";
$bytes2 .= encode_utf8 "$o->{description}\n";
$bytes2 .= encode_utf8 "=> ../rooms/$room->{id}.gmi $room->{name}\n";
$tar->add_data("ijirait/people/$o->{id}.gmi", $bytes2);
}
$tar->add_data("ijirait/rooms/$room->{id}.gmi", $bytes);
}
my $io;
open(my $fh, ">", \$io) or $log->error("Error preparing string for tarball: $!");
$tar->write($fh) or $log->error("Error writing tarball: " . $tar->error);
close($fh);
$stream->write(gzip $io);
}
sub timespan {
my $seconds = shift;
return "some time ago" if not defined $seconds;
return "just now" if $seconds == 0;
return sprintf("%d days ago", int($seconds/86400)) if abs($seconds) > 172800; # 2d
return sprintf("%d hours ago", int($seconds/3600)) if abs($seconds) > 7200; # 2h
return sprintf("%d minutes ago", int($seconds/60)) if abs($seconds) > 120; # 2min
return sprintf("%d seconds ago", $seconds);
}
sub menu {
my $stream = shift;
$stream->write("## Commands\n");
$stream->write("=> /play/ijirait/look look\n");
$stream->write("=> /play/ijirait/say say\n");
$stream->write("=> /play/ijirait/emote emote\n");
$stream->write("=> /play/ijirait/help help\n");
$stream->write("=> /play/ijirait/type type\n");
}
sub help {
my ($stream, $p) = @_;
success($stream);
$stream->write("## Help\n");
my $dir = $server->{wiki_dir};
my $file = "$dir/ijirait-help.gmi";
if (-f $file) {
$stream->write(encode_utf8 read_text($file));
} else {
$stream->write("The help file does not exist.\n");
}
$stream->write("## Automatically Generated Command List\n");
for my $command (sort keys %$commands) {
$stream->write("* $command\n");
}
$stream->write("=> /play/ijirait Back\n");
}
lib/App/Phoebe/Ijirait.pm view on Meta::CPAN
look($stream, $p);
}
sub save {
my ($stream, $p) = @_;
save_world();
success($stream);
$stream->write("# World Save\n");
$stream->write("Data was saved.\n");
$stream->write("=> /play/ijirait Back\n");
}
sub save_world {
cleanup();
my $bytes = encode_json $data;
my $dir = $server->{wiki_dir};
write_binary("$dir/ijirait.json", $bytes);
}
sub cleanup() {
my $now = time;
my %people = map { $_->{location} => 1 } @{$data->{people}};
for my $room (@{$data->{rooms}}) {
my @words;
for my $word (@{$room->{words}}) {
next if $now - $word->{ts} > 600; # don't show messages older than 10min
push(@words, $word);
}
$room->{words} = \@words;
}
}
sub backup() {
my $stream = shift;
my $bytes = encode_json $data;
$bytes =~ s/"fingerprint":"[^"]+"/"fingerprint":""/g;
success($stream, "application/json+gzip");
$stream->write(gzip $bytes);
}
sub who {
my ($stream) = @_;
my $now = time;
success($stream);
$stream->write("# Who are the shape shifters?\n");
for my $o (sort { $b->{ts} <=> $a->{ts} } @{$data->{people}}) {
$o->{name} //= "";
$stream->write(encode_utf8 "* $o->{name}, active " . timespan($now - $o->{ts}) . "\n");
}
$stream->write("=> /play/ijirait Back\n");
}
sub describe {
my ($stream, $p, $text) = @_;
if ($text) {
my ($obj, $description) = split(/\s+/, $text, 2);
if ($obj eq "me") {
$log->debug("Describing $p->{name}");
notify($p, "$p->{name} changes appearance.");
$p->{description} = $description;
my $name = uri_escape_utf8 $p->{name};
result($stream, "30", "/play/ijirait/examine?$name");
return;
}
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
if ($obj eq "room") {
$log->debug("Describing $room->{name}");
notify($p, "$p->{name} changes the roomâs description.");
$room->{description} = $description;
result($stream, "30", "/play/ijirait/look");
return;
}
my $thing = first { $_->{short} eq $obj } @{$room->{things}};
if ($thing) {
$log->debug("Describe $thing->{name}");
notify($p, "$p->{name} changes the description of $thing->{name}.");
$thing->{description} = $description;
my $name = uri_escape_utf8 $thing->{short};
result($stream, "30", "/play/ijirait/examine?$name");
return;
}
# No description of exits.
}
success($stream);
$log->debug("Describing unknown object");
$stream->write(encode_utf8 "# I donât know what to describe\n");
$stream->write(encode_utf8 "The description needs to start with what to describe, e.g. âdescribe me A shape-shifter with red eyes.â\n");
$stream->write(encode_utf8 "You can describe yourself (âmeâ), the room you are in (âroomâ), or a thing (using its shortcut). You cannot describe exits.\n");
$stream->write("=> /play/ijirait Back\n");
}
sub name {
my ($stream, $p, $text) = @_;
if ($text) {
my ($obj, $name) = split(/\s+/, $text, 2);
if ($obj eq "me" and $name !~ /\s/) {
$log->debug("Name $p->{name}");
notify($p, "$p->{name} changes their name to $name.");
$p->{name} = $name;
my $nm = uri_escape_utf8 $p->{name};
result($stream, "30", "/play/ijirait/examine?$nm");
return;
} elsif ($obj eq "room") {
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
$log->debug("Name $room->{name}");
notify($p, "$p->{name} changes the roomâs name to $name.");
$room->{name} = $name;
result($stream, "30", "/play/ijirait/look");
return;
} else {
my $short;
if ($name =~ /(^.*) \((\w+)\)$/) {
$name = $1;
$short = $2;
}
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
my $exit = first { $_->{direction} eq $obj } @{$room->{exits}};
if ($exit) {
$log->debug("Name $exit->{name}");
notify($p, "$p->{name} renames $exit->{direction} to $name ($short).");
$exit->{name} = $name;
$exit->{direction} = $short if $short;
result($stream, "30", "/play/ijirait/look");
return;
}
my $thing = first { $_->{short} eq $obj } @{$room->{things}};
if ($thing) {
$log->debug("Name $thing->{short}");
notify($p, "$p->{name} renames $thing->{name} to $name ($short).");
$thing->{name} = $name;
$thing->{short} = $short if $short;
result($stream, "30", "/play/ijirait/look");
return;
}
}
}
success($stream);
$log->debug("Naming unknown object");
$stream->write(encode_utf8 "# I donât know what to name\n");
$stream->write(encode_utf8 "The command needs to start with what to name, e.g. âname me Sogeeran.â\n");
$stream->write("=> /play/ijirait Back\n");
}
sub create {
my ($stream, $p, $obj) = @_;
if ($obj eq "room") {
$log->debug("Create room");
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
my $dest = new_room();
my $exit = new_exit($room, $dest, $p);
new_exit($dest, $room, $p);
notify($p, "$p->{name} creates a new room.");
result($stream, "30", "/play/ijirait");
} elsif ($obj eq "thing") {
$log->debug("Create thing");
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
new_thing($room, $p);
notify($p, "$p->{name} creates a new thing.");
result($stream, "30", "/play/ijirait");
} else {
lib/App/Phoebe/Ijirait.pm view on Meta::CPAN
}
if ($id and none { $_->{id} == $id } map { $_, @{$_->{things}} } @{$data->{rooms}}) {
success($stream);
$log->debug("Revealing object with impossible condition");
$stream->write(encode_utf8 "# I donât know how to reveal that\n");
$stream->write(encode_utf8 "$id does not refer to a known room or thing.\n");
$stream->write("=> /play/ijirait Back\n");
return;
}
if ($obj) {
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
my $thing = first { $_->{short} eq $obj } @{$room->{things}};
if ($thing) {
if ($thing->{hidden}) {
$log->debug("Reveal '$obj'" . ($id ? " for $id" : ""));
notify($p, "$p->{name} reveals $thing->{name}." . ($id ? " Maybe." : ""));
if ($id) {
$thing->{seen} = $id;
} else {
delete $thing->{hidden};
delete $thing->{seen};
}
}
result($stream, "30", "/play/ijirait/look");
return;
}
my $exit = first { $_->{direction} eq $obj } @{$room->{exits}};
if ($exit) {
if ($exit->{hidden}) {
$log->debug("Reveal '$obj'" . ($id ? " for $id" : ""));
notify($p, "$p->{name} reveals $exit->{name}." . ($id ? " Maybe." : ""));
if ($id) {
$exit->{seen} = $id;
} else {
delete $exit->{hidden};
delete $exit->{seen};
}
}
result($stream, "30", "/play/ijirait");
return;
}
}
success($stream);
$log->debug("Revealing unknown object");
$stream->write(encode_utf8 "# I donât know what to reveal\n");
$stream->write(encode_utf8 "The command needs to use the shortcut of a hidden thing, e.g. âreveal birdâ.\n");
$stream->write(encode_utf8 "To list all the hidden things: âsecretsâ.\n");
$stream->write("=> /play/ijirait Back\n");
}
sub secrets {
my ($stream, $p, $phrase) = @_;
if ($phrase and $phrase eq "are something I do not care for!") {
$log->debug("Secrets");
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
my @things = grep { $_->{hidden} } @{$room->{things}};
my @exits = grep { $_->{hidden} } @{$room->{exits}};
if (@things > 0) {
$stream->write("## Hidden Things\n");
for my $thing (@things) {
my $name = uri_escape_utf8 $thing->{short};
$stream->write(encode_utf8 "=> /play/ijirait/examine?$name $thing->{name} ($thing->{short})\n");
}
}
if (@exits > 0) {
$stream->write("## Hidden Exits\n");
for my $exit (@exits) {
my $direction = uri_escape_utf8 $exit->{direction};
$stream->write(encode_utf8 "=> /play/ijirait/go?$direction $exit->{name} ($exit->{direction})\n");
}
}
if (@things + @exits == 0) {
$stream->write("## No secrets\n");
$stream->write("There are neither hidden things nor hidden exists, here\n");
}
$stream->write("=> /play/ijirait Back\n");
return;
}
success($stream);
$log->debug("Secrets without a passphrase");
$stream->write(encode_utf8 "# Secrets\n");
$stream->write(encode_utf8 "Are you sure you want all the secrets to be revealed? If you are, please use the full command: âsecrets are something I do not care for!â\n");
$stream->write("=> /play/ijirait Back\n");
}
sub find {
my ($stream, $p, $name) = @_;
if (not $name) {
success($stream);
$log->debug("Missing a name in route finding");
$stream->write(encode_utf8 "# Missing a name\n");
$stream->write(encode_utf8 "You need to provide the name of an existing person: âfind <name>â.\n");
$stream->write(encode_utf8 "You can get a list of all existing persons using âwhoâ.\n");
$stream->write("=> /play/ijirait/who Who\n");
$stream->write("=> /play/ijirait Back\n");
return;
}
my $to;
my $o = first { $_->{name} eq $name } @{$data->{people}};
if ($o) {
$to = $o->{location};
} else {
my $room = first { $_->{name} eq $name } @{$data->{rooms}};
$to = $room->{id} if $room;
}
if (not $to) {
success($stream);
$log->debug("Cannot find '$name'");
$stream->write(encode_utf8 "# Cannot find â$nameâ\n");
$stream->write(encode_utf8 "You need to provide the name of an existing room or person: âfind <name>â.\n");
$stream->write(encode_utf8 "You can get a list of all existing rooms using âroomsâ.\n");
$stream->write(encode_utf8 "You can get a list of all existing persons using âwhoâ.\n");
$stream->write("=> /play/ijirait/rooms Rooms\n");
$stream->write("=> /play/ijirait/who Who\n");
$stream->write("=> /play/ijirait Back\n");
return;
}
my $route = find_route($p->{location}, $to);
if (not @$route) {
success($stream);
$log->debug("Cannot find a route to '$name'");
$stream->write(encode_utf8 "# Cannot find a way\n");
$stream->write(encode_utf8 "There seems to be no way to get from here to $name.\n");
$stream->write(encode_utf8 "One of you must use the âconnectâ command to connect back to the rest of the game.\n");
$stream->write("=> /play/ijirait Back\n");
return;
}
success($stream);
$log->debug("Find '$name'");
$stream->write(encode_utf8 "# How to find $name\n");
my $room = uri_escape_utf8 $route->[0]->{direction};
$stream->write(encode_utf8 "=> /play/ijirait/$room $route->[0]->{name} ($route->[0]->{direction})\n");
for (1 .. $#$route) {
$stream->write(encode_utf8 "* $route->[$_]->{name} ($route->[$_]->{direction})\n");
}
$stream->write("=> /play/ijirait Back\n");
}
sub find_route {
my ($from, $to) = @_;
my %rooms = map { $_->{id} => $_ } @{$data->{rooms}};
# breadth first!
my @routes = map { [ $_ ] } @{$rooms{$from}->{exits}};
my $route;
while ($route = shift(@routes)) {
my $last = $route->[$#$route];
return $route if $last->{destination} == $to;
for my $exit (@{$rooms{$last->{destination}}->{exits}}) {
push(@routes, [@$route, $exit]) if none { $exit == $_ } @$route;
}
}
return [];
}
sub id {
my ($stream, $p, $obj) = @_;
success($stream);
if ($obj) {
my $room = first { $_->{id} == $p->{location} } @{$data->{rooms}};
if ($obj eq "room") {
$log->debug("Id $obj");
$stream->write($room->{id} . "\n");
return;
}
my $thing = first { $_->{short} eq $obj } @{$room->{things}};
if ($thing) {
$log->debug("Id '$obj'");
$stream->write($thing->{id} . "\n");
return;
}
}
$log->debug("Id unknown thing");
$stream->write(encode_utf8 "# I donât know what to id\n");
$stream->write(encode_utf8 "The command needs to use the shortcut of a thing, e.g. âid birdâ, or just âid roomâ.\n");
$stream->write("=> /play/ijirait Back\n");
}
sub forget {
my ($stream, $p, $id) = @_;
$log->debug("Forget");
success($stream);
my %seen = map { $_->{id} => $_->{name}, map { $_->{id} => $_->{name} } @{$_->{things}} } @{$data->{rooms}};
if ($id and $seen{$id}) {
$stream->write("# Forgetting\n");
$stream->write(encode_utf8 "You forget ever having seen: $seen{$id} ($id)\n");
$p->{seen} = [grep { $_ != $id } @{$p->{seen}}];
$stream->write("=> /play/ijirait/forget List all the other things you could forget\n");
} elsif ($id) {
$stream->write("# Forgetting the impossible\n");
$stream->write(encode_utf8 "You canât forget what you havenât seen ($id).\n");
} else {
( run in 1.180 second using v1.01-cache-2.11-cpan-6aa56a78535 )