Allow linking a Träwelling account, auto-sync Träwelling→travelynx
travelynx→Träwelling is still work-in-progress Squashed commit of the following: commit 97faa6e2e6c8d20fba30f2d0f6e78187ceeb72e6 Author: Daniel Friesel <derf@finalrewind.org> Date: Wed Sep 30 18:50:05 2020 +0200 improve traewelling log and tx handling commit 487d7dd728b9d45b731bdc7098cf3358ea2e206e Author: Daniel Friesel <derf@finalrewind.org> Date: Wed Sep 30 18:02:41 2020 +0200 add missing traewelling template commit 0148da2f48d9a52dcddc0ab81f83d8f8ac3062ab Author: Daniel Friesel <derf@finalrewind.org> Date: Wed Sep 30 18:02:35 2020 +0200 improve traewelling pull sync commit 4861a9750f9f2d7621043361d0af6b0a8869a0df Author: Daniel Friesel <derf@finalrewind.org> Date: Tue Sep 29 22:14:24 2020 +0200 wip checkin from traewelling commit f6aeb6f06998a2a7a80f63a7b1b688b1a26b66bd Author: Daniel Friesel <derf@finalrewind.org> Date: Tue Sep 29 18:37:53 2020 +0200 refactor traewelling integration. login and logout are less of a hack now. checkin and checkout are not supported at the moment.
This commit is contained in:
parent
952740969c
commit
89e709d8d5
10 changed files with 1213 additions and 41 deletions
251
lib/Travelynx.pm
251
lib/Travelynx.pm
|
@ -20,7 +20,9 @@ use Travelynx::Helper::DBDB;
|
||||||
use Travelynx::Helper::HAFAS;
|
use Travelynx::Helper::HAFAS;
|
||||||
use Travelynx::Helper::IRIS;
|
use Travelynx::Helper::IRIS;
|
||||||
use Travelynx::Helper::Sendmail;
|
use Travelynx::Helper::Sendmail;
|
||||||
|
use Travelynx::Helper::Traewelling;
|
||||||
use Travelynx::Model::Journeys;
|
use Travelynx::Model::Journeys;
|
||||||
|
use Travelynx::Model::Traewelling;
|
||||||
use Travelynx::Model::Users;
|
use Travelynx::Model::Users;
|
||||||
use XML::LibXML;
|
use XML::LibXML;
|
||||||
|
|
||||||
|
@ -292,6 +294,26 @@ sub startup {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$self->helper(
|
||||||
|
traewelling => sub {
|
||||||
|
my ($self) = @_;
|
||||||
|
state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$self->helper(
|
||||||
|
traewelling_api => sub {
|
||||||
|
my ($self) = @_;
|
||||||
|
state $trwl_api = Travelynx::Helper::Traewelling->new(
|
||||||
|
log => $self->app->log,
|
||||||
|
model => $self->traewelling,
|
||||||
|
root_url => $self->url_for('/')->to_abs,
|
||||||
|
user_agent => $self->ua,
|
||||||
|
version => $self->app->config->{version},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
journeys => sub {
|
journeys => sub {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
@ -389,9 +411,12 @@ sub startup {
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
'checkin' => sub {
|
'checkin' => sub {
|
||||||
my ( $self, $station, $train_id, $uid ) = @_;
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
$uid //= $self->current_user->{id};
|
my $station = $opt{station};
|
||||||
|
my $train_id = $opt{train_id};
|
||||||
|
my $uid = $opt{uid} // $self->current_user->{id};
|
||||||
|
my $db = $opt{db} // $self->pg->db;
|
||||||
|
|
||||||
my $status = $self->iris->get_departures(
|
my $status = $self->iris->get_departures(
|
||||||
station => $station,
|
station => $station,
|
||||||
|
@ -409,7 +434,7 @@ sub startup {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
my $user = $self->get_user_status($uid);
|
my $user = $self->get_user_status( $uid, $db );
|
||||||
if ( $user->{checked_in} or $user->{cancelled} ) {
|
if ( $user->{checked_in} or $user->{cancelled} ) {
|
||||||
|
|
||||||
if ( $user->{train_id} eq $train_id
|
if ( $user->{train_id} eq $train_id
|
||||||
|
@ -420,12 +445,17 @@ sub startup {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Otherwise, someone forgot to check out first
|
# Otherwise, someone forgot to check out first
|
||||||
$self->checkout( $station, 1, $uid );
|
$self->checkout(
|
||||||
|
station => $station,
|
||||||
|
force => 1,
|
||||||
|
uid => $uid,
|
||||||
|
db => $db
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
my $json = JSON->new;
|
my $json = JSON->new;
|
||||||
$self->pg->db->insert(
|
$db->insert(
|
||||||
'in_transit',
|
'in_transit',
|
||||||
{
|
{
|
||||||
user_id => $uid,
|
user_id => $uid,
|
||||||
|
@ -459,8 +489,12 @@ sub startup {
|
||||||
"Checkin($uid): INSERT failed: $@");
|
"Checkin($uid): INSERT failed: $@");
|
||||||
return ( undef, 'INSERT failed: ' . $@ );
|
return ( undef, 'INSERT failed: ' . $@ );
|
||||||
}
|
}
|
||||||
$self->add_route_timestamps( $uid, $train, 1 );
|
if ( not $opt{in_transaction} ) {
|
||||||
$self->run_hook( $uid, 'checkin' );
|
|
||||||
|
# mustn't be called during a transaction
|
||||||
|
$self->add_route_timestamps( $uid, $train, 1 );
|
||||||
|
$self->run_hook( $uid, 'checkin' );
|
||||||
|
}
|
||||||
return ( $train, undef );
|
return ( $train, undef );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -547,16 +581,19 @@ sub startup {
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
'checkout' => sub {
|
'checkout' => sub {
|
||||||
my ( $self, $station, $force, $uid ) = @_;
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
my $db = $self->pg->db;
|
my $station = $opt{station};
|
||||||
my $status = $self->iris->get_departures(
|
my $force = $opt{force};
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $db = $opt{db} // $self->pg->db;
|
||||||
|
my $status = $self->iris->get_departures(
|
||||||
station => $station,
|
station => $station,
|
||||||
lookbehind => 120,
|
lookbehind => 120,
|
||||||
lookahead => 120
|
lookahead => 120
|
||||||
);
|
);
|
||||||
$uid //= $self->current_user->{id};
|
$uid //= $self->current_user->{id};
|
||||||
my $user = $self->get_user_status($uid);
|
my $user = $self->get_user_status( $uid, $db );
|
||||||
my $train_id = $user->{train_id};
|
my $train_id = $user->{train_id};
|
||||||
|
|
||||||
if ( not $user->{checked_in} and not $user->{cancelled} ) {
|
if ( not $user->{checked_in} and not $user->{cancelled} ) {
|
||||||
|
@ -671,7 +708,11 @@ sub startup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ( not $force ) {
|
if ( not $force ) {
|
||||||
$self->run_hook( $uid, 'update' );
|
|
||||||
|
# mustn't be called during a transaction
|
||||||
|
if ( not $opt{in_transaction} ) {
|
||||||
|
$self->run_hook( $uid, 'update' );
|
||||||
|
}
|
||||||
return ( 1, undef );
|
return ( 1, undef );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -680,7 +721,10 @@ sub startup {
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
|
|
||||||
my $tx = $db->begin;
|
my $tx;
|
||||||
|
if ( not $opt{in_transaction} ) {
|
||||||
|
$tx = $db->begin;
|
||||||
|
}
|
||||||
|
|
||||||
if ( defined $train and not $train->arrival and not $force ) {
|
if ( defined $train and not $train->arrival and not $force ) {
|
||||||
my $train_no = $train->train_no;
|
my $train_no = $train->train_no;
|
||||||
|
@ -778,7 +822,9 @@ sub startup {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$tx->commit;
|
if ( not $opt{in_transaction} ) {
|
||||||
|
$tx->commit;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($@) {
|
if ($@) {
|
||||||
|
@ -787,27 +833,33 @@ sub startup {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $has_arrived or $force ) {
|
if ( $has_arrived or $force ) {
|
||||||
$self->run_hook( $uid, 'checkout' );
|
if ( not $opt{in_transaction} ) {
|
||||||
|
$self->run_hook( $uid, 'checkout' );
|
||||||
|
}
|
||||||
return ( 0, undef );
|
return ( 0, undef );
|
||||||
}
|
}
|
||||||
$self->run_hook( $uid, 'update' );
|
if ( not $opt{in_transaction} ) {
|
||||||
$self->add_route_timestamps( $uid, $train, 0 );
|
$self->run_hook( $uid, 'update' );
|
||||||
|
$self->add_route_timestamps( $uid, $train, 0 );
|
||||||
|
}
|
||||||
return ( 1, undef );
|
return ( 1, undef );
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
'update_in_transit_comment' => sub {
|
'update_in_transit_comment' => sub {
|
||||||
my ( $self, $comment, $uid ) = @_;
|
my ( $self, $comment, $uid, $db ) = @_;
|
||||||
$uid //= $self->current_user->{id};
|
$uid //= $self->current_user->{id};
|
||||||
|
$db //= $self->pg->db;
|
||||||
|
|
||||||
my $status = $self->pg->db->select( 'in_transit', ['user_data'],
|
my $status
|
||||||
{ user_id => $uid } )->expand->hash;
|
= $db->select( 'in_transit', ['user_data'], { user_id => $uid } )
|
||||||
|
->expand->hash;
|
||||||
if ( not $status ) {
|
if ( not $status ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$status->{user_data}{comment} = $comment;
|
$status->{user_data}{comment} = $comment;
|
||||||
$self->pg->db->update(
|
$db->update(
|
||||||
'in_transit',
|
'in_transit',
|
||||||
{ user_data => JSON->new->encode( $status->{user_data} ) },
|
{ user_data => JSON->new->encode( $status->{user_data} ) },
|
||||||
{ user_id => $uid }
|
{ user_id => $uid }
|
||||||
|
@ -1872,11 +1924,11 @@ sub startup {
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
'get_user_status' => sub {
|
'get_user_status' => sub {
|
||||||
my ( $self, $uid ) = @_;
|
my ( $self, $uid, $db ) = @_;
|
||||||
|
|
||||||
$uid //= $self->current_user->{id};
|
$uid //= $self->current_user->{id};
|
||||||
|
$db //= $self->pg->db;
|
||||||
|
|
||||||
my $db = $self->pg->db;
|
|
||||||
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
|
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
|
||||||
my $epoch = $now->epoch;
|
my $epoch = $now->epoch;
|
||||||
|
|
||||||
|
@ -2315,6 +2367,157 @@ sub startup {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$self->helper(
|
||||||
|
'traewelling_to_travelynx' => sub {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
my $traewelling = $opt{traewelling};
|
||||||
|
my $user_data = $opt{user_data};
|
||||||
|
my $uid = $user_data->{user_id};
|
||||||
|
|
||||||
|
if ( not $traewelling->{checkin}
|
||||||
|
or $self->now->epoch - $traewelling->{checkin}->epoch > 900 )
|
||||||
|
{
|
||||||
|
$self->log->debug("... not checked in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( $traewelling->{status_id}
|
||||||
|
and $user_data->{data}{latest_pull_status_id}
|
||||||
|
and $traewelling->{status_id}
|
||||||
|
== $user_data->{data}{latest_pull_status_id} )
|
||||||
|
{
|
||||||
|
$self->log->debug("... already handled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$self->log->debug("... checked in");
|
||||||
|
my $user_status = $self->get_user_status($uid);
|
||||||
|
if ( $user_status->{checked_in} ) {
|
||||||
|
$self->log->debug(
|
||||||
|
"... also checked in via travelynx. aborting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $traewelling->{category}
|
||||||
|
!~ m{^ (?: nationalExpress | regional | suburban ) $ }x )
|
||||||
|
{
|
||||||
|
$self->log->debug("... status is not a train");
|
||||||
|
$self->traewelling->log(
|
||||||
|
uid => $uid,
|
||||||
|
message =>
|
||||||
|
"$traewelling->{line} nach $traewelling->{arr_name} ist keine Zugfahrt",
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
);
|
||||||
|
$self->traewelling->set_latest_pull_status_id(
|
||||||
|
uid => $uid,
|
||||||
|
status_id => $traewelling->{status_id}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $dep = $self->iris->get_departures(
|
||||||
|
station => $traewelling->{dep_eva},
|
||||||
|
lookbehind => 60,
|
||||||
|
lookahead => 40
|
||||||
|
);
|
||||||
|
if ( $dep->{errstr} ) {
|
||||||
|
$self->traewelling->log(
|
||||||
|
uid => $uid,
|
||||||
|
message =>
|
||||||
|
"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $dep->{errstr}",
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
is_error => 1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
my ( $train_ref, $train_id );
|
||||||
|
for my $train ( @{ $dep->{results} } ) {
|
||||||
|
if ( $train->line ne $traewelling->{line} ) {
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
if ( not $train->sched_departure
|
||||||
|
or $train->sched_departure->epoch
|
||||||
|
!= $traewelling->{dep_dt}->epoch )
|
||||||
|
{
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
not List::Util::first { $_ eq $traewelling->{arr_name} }
|
||||||
|
$train->route_post
|
||||||
|
)
|
||||||
|
{
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$train_id = $train->train_id;
|
||||||
|
$train_ref = $train;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
if ($train_id) {
|
||||||
|
$self->log->debug("... found train: $train_id");
|
||||||
|
|
||||||
|
my $db = $self->pg->db;
|
||||||
|
my $tx = $db->begin;
|
||||||
|
|
||||||
|
my ( undef, $err ) = $self->checkin(
|
||||||
|
station => $traewelling->{dep_eva},
|
||||||
|
train_id => $train_id,
|
||||||
|
uid => $uid,
|
||||||
|
in_transaction => 1,
|
||||||
|
db => $db
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( not $err ) {
|
||||||
|
( undef, $err ) = $self->checkout(
|
||||||
|
station => $traewelling->{arr_eva},
|
||||||
|
train_id => 0,
|
||||||
|
uid => $uid,
|
||||||
|
in_transaction => 1,
|
||||||
|
db => $db
|
||||||
|
);
|
||||||
|
if ( not $err ) {
|
||||||
|
$self->log->debug("... success!");
|
||||||
|
if ( $traewelling->{message} ) {
|
||||||
|
$self->update_in_transit_comment(
|
||||||
|
$traewelling->{message},
|
||||||
|
$uid, $db );
|
||||||
|
}
|
||||||
|
$self->traewelling->log(
|
||||||
|
uid => $uid,
|
||||||
|
db => $db,
|
||||||
|
message =>
|
||||||
|
"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
);
|
||||||
|
$self->traewelling->set_latest_pull_status_id(
|
||||||
|
uid => $uid,
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
db => $db
|
||||||
|
);
|
||||||
|
|
||||||
|
$tx->commit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($err) {
|
||||||
|
$self->log->debug("... error: $err");
|
||||||
|
$self->traewelling->log(
|
||||||
|
uid => $uid,
|
||||||
|
message =>
|
||||||
|
"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $err",
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
is_error => 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$self->traewelling->log(
|
||||||
|
uid => $uid,
|
||||||
|
message =>
|
||||||
|
"$traewelling->{line} nach $traewelling->{arr_name} nicht gefunden",
|
||||||
|
status_id => $traewelling->{status_id},
|
||||||
|
is_error => 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$self->helper(
|
$self->helper(
|
||||||
'journeys_to_map_data' => sub {
|
'journeys_to_map_data' => sub {
|
||||||
my ( $self, %opt ) = @_;
|
my ( $self, %opt ) = @_;
|
||||||
|
@ -2647,6 +2850,7 @@ sub startup {
|
||||||
$authed_r->get('/account')->to('account#account');
|
$authed_r->get('/account')->to('account#account');
|
||||||
$authed_r->get('/account/privacy')->to('account#privacy');
|
$authed_r->get('/account/privacy')->to('account#privacy');
|
||||||
$authed_r->get('/account/hooks')->to('account#webhook');
|
$authed_r->get('/account/hooks')->to('account#webhook');
|
||||||
|
$authed_r->get('/account/traewelling')->to('traewelling#settings');
|
||||||
$authed_r->get('/account/insight')->to('account#insight');
|
$authed_r->get('/account/insight')->to('account#insight');
|
||||||
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
|
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
|
||||||
$authed_r->get('/cancelled')->to('traveling#cancelled');
|
$authed_r->get('/cancelled')->to('traveling#cancelled');
|
||||||
|
@ -2668,6 +2872,7 @@ sub startup {
|
||||||
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
|
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
|
||||||
$authed_r->post('/account/privacy')->to('account#privacy');
|
$authed_r->post('/account/privacy')->to('account#privacy');
|
||||||
$authed_r->post('/account/hooks')->to('account#webhook');
|
$authed_r->post('/account/hooks')->to('account#webhook');
|
||||||
|
$authed_r->post('/account/traewelling')->to('traewelling#settings');
|
||||||
$authed_r->post('/account/insight')->to('account#insight');
|
$authed_r->post('/account/insight')->to('account#insight');
|
||||||
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
|
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
|
||||||
$authed_r->post('/journey/comment')->to('traveling#comment_form');
|
$authed_r->post('/journey/comment')->to('traveling#comment_form');
|
||||||
|
|
|
@ -1012,6 +1012,32 @@ my @migrations = (
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# v21 -> v22
|
||||||
|
sub {
|
||||||
|
my ($db) = @_;
|
||||||
|
$db->query(
|
||||||
|
qq{
|
||||||
|
create table traewelling (
|
||||||
|
user_id integer not null references users (id) primary key,
|
||||||
|
email varchar(256) not null,
|
||||||
|
push_sync boolean not null,
|
||||||
|
pull_sync boolean not null,
|
||||||
|
errored boolean,
|
||||||
|
token text,
|
||||||
|
data jsonb,
|
||||||
|
latest_run timestamptz
|
||||||
|
);
|
||||||
|
comment on table traewelling is 'Token and Status for Traewelling';
|
||||||
|
create view traewelling_str as select
|
||||||
|
user_id, email, push_sync, pull_sync, errored, token, data,
|
||||||
|
extract(epoch from latest_run) as latest_run_ts
|
||||||
|
from traewelling
|
||||||
|
;
|
||||||
|
update schema_version set version = 22;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
sub setup_db {
|
sub setup_db {
|
||||||
|
|
|
@ -108,7 +108,11 @@ sub run {
|
||||||
|
|
||||||
# check out (adds a cancelled journey and resets journey state
|
# check out (adds a cancelled journey and resets journey state
|
||||||
# to checkin
|
# to checkin
|
||||||
$self->app->checkout( $arr, 1, $uid );
|
$self->app->checkout(
|
||||||
|
station => $arr,
|
||||||
|
force => 1,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -201,7 +205,11 @@ sub run {
|
||||||
{
|
{
|
||||||
# check out (adds a cancelled journey and resets journey state
|
# check out (adds a cancelled journey and resets journey state
|
||||||
# to destination selection)
|
# to destination selection)
|
||||||
$self->app->checkout( $arr, 0, $uid );
|
$self->app->checkout(
|
||||||
|
station => $arr,
|
||||||
|
force => 0,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -209,7 +217,11 @@ sub run {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elsif ( $entry->{real_arr_ts} ) {
|
elsif ( $entry->{real_arr_ts} ) {
|
||||||
my ( undef, $error ) = $self->app->checkout( $arr, 1, $uid );
|
my ( undef, $error ) = $self->app->checkout(
|
||||||
|
station => $arr,
|
||||||
|
force => 1,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
if ($error) {
|
if ($error) {
|
||||||
die("${error}\n");
|
die("${error}\n");
|
||||||
}
|
}
|
||||||
|
@ -222,6 +234,31 @@ sub run {
|
||||||
eval { }
|
eval { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for my $account_data ( $self->app->traewelling->get_pull_accounts ) {
|
||||||
|
|
||||||
|
# $account_data->{user_id} is the travelynx uid
|
||||||
|
# $account_data->{user_name} is the Träwelling username
|
||||||
|
$self->app->log->debug(
|
||||||
|
"Pulling Traewelling status for UID $account_data->{user_id}");
|
||||||
|
$self->app->traewelling_api->get_status_p(
|
||||||
|
username => $account_data->{data}{user_name},
|
||||||
|
token => $account_data->{token}
|
||||||
|
)->then(
|
||||||
|
sub {
|
||||||
|
my ($traewelling) = @_;
|
||||||
|
$self->app->traewelling_to_travelynx(
|
||||||
|
traewelling => $traewelling,
|
||||||
|
user_data => $account_data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$self->app->log->debug("Error $err");
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
}
|
||||||
|
|
||||||
# Computing yearly stats may take a while, but we've got all time in the
|
# Computing yearly stats may take a while, but we've got all time in the
|
||||||
# world here. This means users won't have to wait when loading their
|
# world here. This means users won't have to wait when loading their
|
||||||
# own by-year journey log.
|
# own by-year journey log.
|
||||||
|
@ -232,6 +269,8 @@ sub run {
|
||||||
year => $now->year
|
year => $now->year
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO wait until all background jobs have terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -258,14 +258,21 @@ sub travel_v1 {
|
||||||
$train_id = $train->train_id;
|
$train_id = $train->train_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
my ( $train, $error )
|
my ( $train, $error ) = $self->checkin(
|
||||||
= $self->checkin( $from_station, $train_id, $uid );
|
station => $from_station,
|
||||||
|
train_id => $train_id,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
if ( $payload->{comment} and not $error ) {
|
if ( $payload->{comment} and not $error ) {
|
||||||
$self->update_in_transit_comment(
|
$self->update_in_transit_comment(
|
||||||
sanitize( q{}, $payload->{comment} ), $uid );
|
sanitize( q{}, $payload->{comment} ), $uid );
|
||||||
}
|
}
|
||||||
if ( $to_station and not $error ) {
|
if ( $to_station and not $error ) {
|
||||||
( $train, $error ) = $self->checkout( $to_station, 0, $uid );
|
( $train, $error ) = $self->checkout(
|
||||||
|
station => $to_station,
|
||||||
|
force => 0,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ($error) {
|
if ($error) {
|
||||||
$self->render(
|
$self->render(
|
||||||
|
@ -307,8 +314,11 @@ sub travel_v1 {
|
||||||
sanitize( q{}, $payload->{comment} ), $uid );
|
sanitize( q{}, $payload->{comment} ), $uid );
|
||||||
}
|
}
|
||||||
|
|
||||||
my ( $train, $error )
|
my ( $train, $error ) = $self->checkout(
|
||||||
= $self->checkout( $to_station, $payload->{force} ? 1 : 0, $uid );
|
station => $to_station,
|
||||||
|
force => $payload->{force} ? 1 : 0,
|
||||||
|
uid => $uid
|
||||||
|
);
|
||||||
if ($error) {
|
if ($error) {
|
||||||
$self->render(
|
$self->render(
|
||||||
json => {
|
json => {
|
||||||
|
|
104
lib/Travelynx/Controller/Traewelling.pm
Normal file
104
lib/Travelynx/Controller/Traewelling.pm
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package Travelynx::Controller::Traewelling;
|
||||||
|
use Mojo::Base 'Mojolicious::Controller';
|
||||||
|
use Mojo::Promise;
|
||||||
|
|
||||||
|
sub settings {
|
||||||
|
my ($self) = @_;
|
||||||
|
|
||||||
|
my $uid = $self->current_user->{id};
|
||||||
|
|
||||||
|
if ( $self->param('action')
|
||||||
|
and $self->validation->csrf_protect->has_error('csrf_token') )
|
||||||
|
{
|
||||||
|
$self->render(
|
||||||
|
'traewelling',
|
||||||
|
invalid => 'csrf',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $self->param('action') and $self->param('action') eq 'login' ) {
|
||||||
|
my $email = $self->param('email');
|
||||||
|
my $password = $self->param('password');
|
||||||
|
$self->render_later;
|
||||||
|
$self->traewelling_api->login_p(
|
||||||
|
uid => $uid,
|
||||||
|
email => $email,
|
||||||
|
password => $password
|
||||||
|
)->then(
|
||||||
|
sub {
|
||||||
|
my $traewelling = $self->traewelling->get($uid);
|
||||||
|
$self->param( sync_source => 'none' );
|
||||||
|
$self->render(
|
||||||
|
'traewelling',
|
||||||
|
traewelling => $traewelling,
|
||||||
|
new_traewelling => 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$self->render(
|
||||||
|
'traewelling',
|
||||||
|
traewelling => {},
|
||||||
|
new_traewelling => 1,
|
||||||
|
login_error => $err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elsif ( $self->param('action') and $self->param('action') eq 'logout' ) {
|
||||||
|
$self->render_later;
|
||||||
|
my $traewelling = $self->traewelling->get($uid);
|
||||||
|
$self->traewelling_api->logout_p(
|
||||||
|
uid => $uid,
|
||||||
|
token => $traewelling->{token}
|
||||||
|
)->then(
|
||||||
|
sub {
|
||||||
|
$self->flash( success => 'traewelling' );
|
||||||
|
$self->redirect_to('account');
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$self->render(
|
||||||
|
'traewelling',
|
||||||
|
traewelling => {},
|
||||||
|
new_traewelling => 1,
|
||||||
|
logout_error => $err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elsif ( $self->param('action') and $self->param('action') eq 'config' ) {
|
||||||
|
$self->traewelling->set_sync(
|
||||||
|
uid => $uid,
|
||||||
|
push_sync => $self->param('sync_source') eq 'travelynx' ? 1 : 0,
|
||||||
|
pull_sync => $self->param('sync_source') eq 'traewelling' ? 1 : 0
|
||||||
|
);
|
||||||
|
$self->flash( success => 'traewelling' );
|
||||||
|
$self->redirect_to('account');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $traewelling = $self->traewelling->get($uid);
|
||||||
|
|
||||||
|
if ( $traewelling->{push_sync} ) {
|
||||||
|
$self->param( sync_source => 'travelynx' );
|
||||||
|
}
|
||||||
|
elsif ( $traewelling->{pull_sync} ) {
|
||||||
|
$self->param( sync_source => 'traewelling' );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$self->param( sync_source => 'none' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->render(
|
||||||
|
'traewelling',
|
||||||
|
traewelling => $traewelling,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
|
@ -424,8 +424,10 @@ sub log_action {
|
||||||
|
|
||||||
if ( $params->{action} eq 'checkin' ) {
|
if ( $params->{action} eq 'checkin' ) {
|
||||||
|
|
||||||
my ( $train, $error )
|
my ( $train, $error ) = $self->checkin(
|
||||||
= $self->checkin( $params->{station}, $params->{train} );
|
station => $params->{station},
|
||||||
|
train_id => $params->{train}
|
||||||
|
);
|
||||||
my $destination = $params->{dest};
|
my $destination = $params->{dest};
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
|
@ -447,8 +449,10 @@ sub log_action {
|
||||||
else {
|
else {
|
||||||
# Silently ignore errors -- if they are permanent, the user will see
|
# Silently ignore errors -- if they are permanent, the user will see
|
||||||
# them when selecting the destination manually.
|
# them when selecting the destination manually.
|
||||||
my ( $still_checked_in, undef )
|
my ( $still_checked_in, undef ) = $self->checkout(
|
||||||
= $self->checkout( $destination, 0 );
|
station => $destination,
|
||||||
|
force => 0
|
||||||
|
);
|
||||||
my $station_link = '/s/' . $destination;
|
my $station_link = '/s/' . $destination;
|
||||||
$self->render(
|
$self->render(
|
||||||
json => {
|
json => {
|
||||||
|
@ -459,8 +463,10 @@ sub log_action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elsif ( $params->{action} eq 'checkout' ) {
|
elsif ( $params->{action} eq 'checkout' ) {
|
||||||
my ( $still_checked_in, $error )
|
my ( $still_checked_in, $error ) = $self->checkout(
|
||||||
= $self->checkout( $params->{station}, $params->{force} );
|
station => $params->{station},
|
||||||
|
force => $params->{force}
|
||||||
|
);
|
||||||
my $station_link = '/s/' . $params->{station};
|
my $station_link = '/s/' . $params->{station};
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
|
@ -505,8 +511,10 @@ sub log_action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elsif ( $params->{action} eq 'cancelled_from' ) {
|
elsif ( $params->{action} eq 'cancelled_from' ) {
|
||||||
my ( undef, $error )
|
my ( undef, $error ) = $self->checkin(
|
||||||
= $self->checkin( $params->{station}, $params->{train} );
|
station => $params->{station},
|
||||||
|
train_id => $params->{train}
|
||||||
|
);
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
$self->render(
|
$self->render(
|
||||||
|
@ -526,8 +534,10 @@ sub log_action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elsif ( $params->{action} eq 'cancelled_to' ) {
|
elsif ( $params->{action} eq 'cancelled_to' ) {
|
||||||
my ( undef, $error )
|
my ( undef, $error ) = $self->checkout(
|
||||||
= $self->checkout( $params->{station}, 1 );
|
station => $params->{station},
|
||||||
|
force => 1
|
||||||
|
);
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
$self->render(
|
$self->render(
|
||||||
|
|
332
lib/Travelynx/Helper/Traewelling.pm
Normal file
332
lib/Travelynx/Helper/Traewelling.pm
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
package Travelynx::Helper::Traewelling;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use 5.020;
|
||||||
|
|
||||||
|
use Mojo::Promise;
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ( $class, %opt ) = @_;
|
||||||
|
|
||||||
|
my $version = $opt{version};
|
||||||
|
|
||||||
|
$opt{header}
|
||||||
|
= { 'User-Agent' =>
|
||||||
|
"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
|
||||||
|
};
|
||||||
|
|
||||||
|
return bless( \%opt, $class );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_status_p {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $username = $opt{username};
|
||||||
|
my $token = $opt{token};
|
||||||
|
my $promise = Mojo::Promise->new;
|
||||||
|
|
||||||
|
my $header = {
|
||||||
|
'User-Agent' => $self->{header}{'User-Agent'},
|
||||||
|
'Authorization' => "Bearer $token",
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->{user_agent}->request_timeout(20)
|
||||||
|
->get_p( "https://traewelling.de/api/v0/user/${username}" => $header )
|
||||||
|
->then(
|
||||||
|
sub {
|
||||||
|
my ($tx) = @_;
|
||||||
|
if ( my $err = $tx->error ) {
|
||||||
|
my $err_msg = "HTTP $err->{code} $err->{message}";
|
||||||
|
$promise->reject($err_msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ( my $status = $tx->result->json->{statuses}{data}[0] ) {
|
||||||
|
my $strp = DateTime::Format::Strptime->new(
|
||||||
|
pattern => '%Y-%m-%dT%H:%M:%S.000000Z',
|
||||||
|
time_zone => 'UTC',
|
||||||
|
);
|
||||||
|
my $status_id = $status->{id};
|
||||||
|
my $message = $status->{body};
|
||||||
|
my $checkin_at
|
||||||
|
= $strp->parse_datetime( $status->{created_at} );
|
||||||
|
|
||||||
|
my $dep_dt = $strp->parse_datetime(
|
||||||
|
$status->{train_checkin}{departure} );
|
||||||
|
my $arr_dt = $strp->parse_datetime(
|
||||||
|
$status->{train_checkin}{arrival} );
|
||||||
|
|
||||||
|
my $dep_eva
|
||||||
|
= $status->{train_checkin}{origin}{ibnr};
|
||||||
|
my $arr_eva
|
||||||
|
= $status->{train_checkin}{destination}{ibnr};
|
||||||
|
|
||||||
|
my $dep_name
|
||||||
|
= $status->{train_checkin}{origin}{name};
|
||||||
|
my $arr_name
|
||||||
|
= $status->{train_checkin}{destination}{name};
|
||||||
|
|
||||||
|
my $category
|
||||||
|
= $status->{train_checkin}{hafas_trip}{category};
|
||||||
|
my $trip_id
|
||||||
|
= $status->{train_checkin}{hafas_trip}{trip_id};
|
||||||
|
my $linename
|
||||||
|
= $status->{train_checkin}{hafas_trip}{linename};
|
||||||
|
my ( $train_type, $train_line ) = split( qr{ }, $linename );
|
||||||
|
$promise->resolve(
|
||||||
|
{
|
||||||
|
status_id => $status_id,
|
||||||
|
message => $message,
|
||||||
|
checkin => $checkin_at,
|
||||||
|
dep_dt => $dep_dt,
|
||||||
|
dep_eva => $dep_eva,
|
||||||
|
dep_name => $dep_name,
|
||||||
|
arr_dt => $arr_dt,
|
||||||
|
arr_eva => $arr_eva,
|
||||||
|
arr_name => $arr_name,
|
||||||
|
trip_id => $trip_id,
|
||||||
|
train_type => $train_type,
|
||||||
|
line => $linename,
|
||||||
|
line_no => $train_line,
|
||||||
|
category => $category,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$promise->reject("unknown error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$promise->reject($err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
|
||||||
|
return $promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_user_p {
|
||||||
|
my ( $self, $uid, $token ) = @_;
|
||||||
|
my $ua = $self->{user_agent}->request_timeout(20);
|
||||||
|
|
||||||
|
my $header = {
|
||||||
|
'User-Agent' => $self->{header}{'User-Agent'},
|
||||||
|
'Authorization' => "Bearer $token",
|
||||||
|
};
|
||||||
|
my $promise = Mojo::Promise->new;
|
||||||
|
|
||||||
|
$ua->get_p( "https://traewelling.de/api/v0/getuser" => $header )->then(
|
||||||
|
sub {
|
||||||
|
my ($tx) = @_;
|
||||||
|
if ( my $err = $tx->error ) {
|
||||||
|
my $err_msg
|
||||||
|
= "HTTP $err->{code} $err->{message} bei Abfrage der Nutzerdaten";
|
||||||
|
$promise->reject($err_msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
my $user_data = $tx->result->json;
|
||||||
|
$self->{model}->set_user(
|
||||||
|
uid => $uid,
|
||||||
|
trwl_id => $user_data->{id},
|
||||||
|
screen_name => $user_data->{name},
|
||||||
|
user_name => $user_data->{username},
|
||||||
|
);
|
||||||
|
$promise->resolve;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$promise->reject("$err bei Abfrage der Nutzerdaten");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
|
||||||
|
return $promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub login_p {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $email = $opt{email};
|
||||||
|
my $password = $opt{password};
|
||||||
|
|
||||||
|
my $ua = $self->{user_agent}->request_timeout(20);
|
||||||
|
|
||||||
|
my $request = {
|
||||||
|
email => $email,
|
||||||
|
password => $password,
|
||||||
|
};
|
||||||
|
|
||||||
|
my $promise = Mojo::Promise->new;
|
||||||
|
my $token;
|
||||||
|
|
||||||
|
$ua->post_p(
|
||||||
|
"https://traewelling.de/api/v0/auth/login" => $self->{header} =>
|
||||||
|
json => $request )->then(
|
||||||
|
sub {
|
||||||
|
my ($tx) = @_;
|
||||||
|
if ( my $err = $tx->error ) {
|
||||||
|
my $err_msg = "HTTP $err->{code} $err->{message} bei Login";
|
||||||
|
$promise->reject($err_msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$token = $tx->result->json->{token};
|
||||||
|
$self->{model}->link(
|
||||||
|
uid => $uid,
|
||||||
|
email => $email,
|
||||||
|
token => $token
|
||||||
|
);
|
||||||
|
return $self->get_user_p( $uid, $token );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)->then(
|
||||||
|
sub {
|
||||||
|
$promise->resolve;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
if ($token) {
|
||||||
|
|
||||||
|
# We have a token, but couldn't complete the login. For now, we
|
||||||
|
# solve this by logging out and invalidating the token.
|
||||||
|
$self->logout_p(
|
||||||
|
uid => $uid,
|
||||||
|
token => $token
|
||||||
|
)->finally(
|
||||||
|
sub {
|
||||||
|
$promise->reject($err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$promise->reject($err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
|
||||||
|
return $promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub logout_p {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $token = $opt{token};
|
||||||
|
|
||||||
|
my $ua = $self->{user_agent}->request_timeout(20);
|
||||||
|
|
||||||
|
my $header = {
|
||||||
|
'User-Agent' => $self->{header}{'User-Agent'},
|
||||||
|
'Authorization' => "Bearer $token",
|
||||||
|
};
|
||||||
|
my $request = {};
|
||||||
|
|
||||||
|
$self->{model}->unlink( uid => $uid );
|
||||||
|
|
||||||
|
my $promise = Mojo::Promise->new;
|
||||||
|
|
||||||
|
$ua->post_p(
|
||||||
|
"https://traewelling.de/api/v0/auth/logout" => $header => json =>
|
||||||
|
$request )->then(
|
||||||
|
sub {
|
||||||
|
my ($tx) = @_;
|
||||||
|
if ( my $err = $tx->error ) {
|
||||||
|
my $err_msg = "HTTP $err->{code} $err->{message}";
|
||||||
|
$promise->reject($err_msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$promise->resolve;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$promise->reject($err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
|
||||||
|
return $promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub checkin {
|
||||||
|
my ( $self, $uid ) = @_;
|
||||||
|
if ( my $token = $self->get_traewelling_push_token($uid) ) {
|
||||||
|
my $user = $self->get_user_status;
|
||||||
|
|
||||||
|
# TODO delete previous traewelling status if the train's destination has been changed
|
||||||
|
# TODO delete traewelling status when undoing a travelynx checkin
|
||||||
|
if ( $user->{checked_in} and $user->{extra_data}{trip_id} ) {
|
||||||
|
my $traewelling = $self->{model}->get($uid);
|
||||||
|
if ( $traewelling->{data}{trip_id} eq $user->{extra_data}{trip_id} )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
my $header = {
|
||||||
|
'User-Agent' => 'travelynx/' . $self->{version},
|
||||||
|
'Authorization' => "Bearer $token",
|
||||||
|
};
|
||||||
|
|
||||||
|
my $request = {
|
||||||
|
tripID => $user->{extra_data}{trip_id},
|
||||||
|
start => q{} . $user->{dep_eva},
|
||||||
|
destination => q{} . $user->{arr_eva},
|
||||||
|
};
|
||||||
|
my $trip_req = sprintf(
|
||||||
|
"tripID=%s&lineName=%s%%20%s&start=%s",
|
||||||
|
$user->{extra_data}{trip_id}, $user->{train_type},
|
||||||
|
$user->{train_line} // $user->{train_no}, $user->{dep_eva}
|
||||||
|
);
|
||||||
|
$self->{user_agent}->request_timeout(20)
|
||||||
|
->get_p(
|
||||||
|
"https://traewelling.de/api/v0/trains/trip?$trip_req" =>
|
||||||
|
$header )->then(
|
||||||
|
sub {
|
||||||
|
return $self->{user_agent}->request_timeout(20)
|
||||||
|
->post_p(
|
||||||
|
"https://traewelling.de/api/v0/trains/checkin" =>
|
||||||
|
$header => json => $request );
|
||||||
|
}
|
||||||
|
)->then(
|
||||||
|
sub {
|
||||||
|
my ($tx) = @_;
|
||||||
|
if ( my $err = $tx->error ) {
|
||||||
|
my $err_msg = "HTTP $err->{code} $err->{message}";
|
||||||
|
$self->mark_trwl_checkin_error( $uid, $user, $err_msg );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# TODO check for traewelling error ("error" key in response)
|
||||||
|
# TODO store ID of resulting status (request /user/{name} and store status ID)
|
||||||
|
$self->mark_trwl_checkin_success( $uid, $user );
|
||||||
|
|
||||||
|
# mark success: checked into (trip_id, start, destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)->catch(
|
||||||
|
sub {
|
||||||
|
my ($err) = @_;
|
||||||
|
$self->mark_trwl_checkin_error( $uid, $user, $err );
|
||||||
|
}
|
||||||
|
)->wait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
204
lib/Travelynx/Model/Traewelling.pm
Normal file
204
lib/Travelynx/Model/Traewelling.pm
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package Travelynx::Model::Traewelling;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use 5.020;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
sub epoch_to_dt {
|
||||||
|
my ($epoch) = @_;
|
||||||
|
|
||||||
|
# Bugs (and user errors) may lead to undefined timestamps. Set them to
|
||||||
|
# 1970-01-01 to avoid crashing and show obviously wrong data instead.
|
||||||
|
$epoch //= 0;
|
||||||
|
|
||||||
|
return DateTime->from_epoch(
|
||||||
|
epoch => $epoch,
|
||||||
|
time_zone => 'Europe/Berlin',
|
||||||
|
locale => 'de-DE',
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ( $class, %opt ) = @_;
|
||||||
|
|
||||||
|
return bless( \%opt, $class );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub now {
|
||||||
|
return DateTime->now( time_zone => 'Europe/Berlin' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub link {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $log = [ [ $self->now->epoch, "Erfolgreich angemeldet" ] ];
|
||||||
|
|
||||||
|
my $data = {
|
||||||
|
user_id => $opt{uid},
|
||||||
|
email => $opt{email},
|
||||||
|
push_sync => 0,
|
||||||
|
pull_sync => 0,
|
||||||
|
token => $opt{token},
|
||||||
|
data => JSON->new->encode( { log => $log } ),
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->{pg}->db->insert(
|
||||||
|
'traewelling',
|
||||||
|
$data,
|
||||||
|
{
|
||||||
|
on_conflict => \
|
||||||
|
'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set_user {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $res_h
|
||||||
|
= $self->{pg}
|
||||||
|
->db->select( 'traewelling', 'data', { user_id => $opt{uid} } )
|
||||||
|
->expand->hash;
|
||||||
|
|
||||||
|
$res_h->{data}{user_id} = $opt{trwl_id};
|
||||||
|
$res_h->{data}{screen_name} = $opt{screen_name};
|
||||||
|
$res_h->{data}{user_name} = $opt{user_name};
|
||||||
|
|
||||||
|
$self->{pg}->db->update(
|
||||||
|
'traewelling',
|
||||||
|
{ data => JSON->new->encode( $res_h->{data} ) },
|
||||||
|
{ user_id => $opt{uid} }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub unlink {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
|
||||||
|
$self->{pg}->db->delete( 'traewelling', { user_id => $uid } );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get {
|
||||||
|
my ( $self, $uid ) = @_;
|
||||||
|
$uid //= $self->current_user->{id};
|
||||||
|
|
||||||
|
my $res_h
|
||||||
|
= $self->{pg}->db->select( 'traewelling_str', '*', { user_id => $uid } )
|
||||||
|
->expand->hash;
|
||||||
|
|
||||||
|
$res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} );
|
||||||
|
for my $log_entry ( @{ $res_h->{data}{log} // [] } ) {
|
||||||
|
$log_entry->[0] = epoch_to_dt( $log_entry->[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub log {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $message = $opt{message};
|
||||||
|
my $is_error = $opt{is_error};
|
||||||
|
my $db = $opt{db} // $self->{pg}->db;
|
||||||
|
my $res_h
|
||||||
|
= $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
|
||||||
|
splice( @{ $res_h->{data}{log} // [] }, 9 );
|
||||||
|
unshift(
|
||||||
|
@{ $res_h->{data}{log} },
|
||||||
|
[ $self->now->epoch, $message, $opt{status_id} ]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($is_error) {
|
||||||
|
$res_h->{data}{error} = $message;
|
||||||
|
}
|
||||||
|
$db->update(
|
||||||
|
'traewelling',
|
||||||
|
{
|
||||||
|
errored => $is_error ? 1 : 0,
|
||||||
|
latest_run => $self->now,
|
||||||
|
data => JSON->new->encode( $res_h->{data} )
|
||||||
|
},
|
||||||
|
{ user_id => $uid }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set_latest_pull_status_id {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $status_id = $opt{status_id};
|
||||||
|
my $db = $opt{db} // $self->{pg}->db;
|
||||||
|
|
||||||
|
my $res_h
|
||||||
|
= $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
|
||||||
|
|
||||||
|
$res_h->{data}{latest_pull_status_id} = $status_id;
|
||||||
|
|
||||||
|
$db->update(
|
||||||
|
'traewelling',
|
||||||
|
{ data => JSON->new->encode( $res_h->{data} ) },
|
||||||
|
{ user_id => $uid }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set_latest_push_status_id {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $status_id = $opt{status_id};
|
||||||
|
my $db = $opt{db} // $self->{pg}->db;
|
||||||
|
|
||||||
|
my $res_h
|
||||||
|
= $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
|
||||||
|
|
||||||
|
$res_h->{data}{latest_push_status_id} = $status_id;
|
||||||
|
|
||||||
|
$db->update(
|
||||||
|
'traewelling',
|
||||||
|
{ data => JSON->new->encode( $res_h->{data} ) },
|
||||||
|
{ user_id => $uid }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set_sync {
|
||||||
|
my ( $self, %opt ) = @_;
|
||||||
|
|
||||||
|
my $uid = $opt{uid};
|
||||||
|
my $push_sync = $opt{push_sync};
|
||||||
|
my $pull_sync = $opt{pull_sync};
|
||||||
|
|
||||||
|
$self->{pg}->db->update(
|
||||||
|
'traewelling',
|
||||||
|
{
|
||||||
|
push_sync => $push_sync,
|
||||||
|
pull_sync => $pull_sync
|
||||||
|
},
|
||||||
|
{ user_id => $uid }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_push_accounts {
|
||||||
|
my ($self) = @_;
|
||||||
|
my $res = $self->{pg}->db->select(
|
||||||
|
'traewelling',
|
||||||
|
[ 'user_id', 'token', 'data' ],
|
||||||
|
{ push_sync => 1 }
|
||||||
|
);
|
||||||
|
return $res->expand->hashes->each;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_pull_accounts {
|
||||||
|
my ($self) = @_;
|
||||||
|
my $res = $self->{pg}->db->select(
|
||||||
|
'traewelling',
|
||||||
|
[ 'user_id', 'token', 'data' ],
|
||||||
|
{ pull_sync => 1 }
|
||||||
|
);
|
||||||
|
return $res->expand->hashes->each;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
|
@ -16,6 +16,9 @@
|
||||||
% elsif ($success eq 'privacy') {
|
% elsif ($success eq 'privacy') {
|
||||||
<span class="card-title">Einstellungen zu öffentlichen Account-Daten geändert</span>
|
<span class="card-title">Einstellungen zu öffentlichen Account-Daten geändert</span>
|
||||||
% }
|
% }
|
||||||
|
% elsif ($success eq 'traewelling') {
|
||||||
|
<span class="card-title">Traewelling-Verknüpfung aktualisiert</span>
|
||||||
|
% }
|
||||||
% elsif ($success eq 'use_history') {
|
% elsif ($success eq 'use_history') {
|
||||||
<span class="card-title">Einstellungen zu vorgeschlagenen Verbindungen geändert</span>
|
<span class="card-title">Einstellungen zu vorgeschlagenen Verbindungen geändert</span>
|
||||||
% }
|
% }
|
||||||
|
@ -31,6 +34,7 @@
|
||||||
<h1>Account</h1>
|
<h1>Account</h1>
|
||||||
% my $acc = current_user();
|
% my $acc = current_user();
|
||||||
% my $hook = get_webhook();
|
% my $hook = get_webhook();
|
||||||
|
% my $traewelling = traewelling->get($acc->{id});
|
||||||
% my $use_history = users->use_history(uid => $acc->{id});
|
% my $use_history = users->use_history(uid => $acc->{id});
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
|
@ -110,6 +114,24 @@
|
||||||
% }
|
% }
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Traewelling</th>
|
||||||
|
<td>
|
||||||
|
<a href="/account/traewelling"><i class="material-icons">edit</i></a>
|
||||||
|
% if (not ($traewelling->{token})) {
|
||||||
|
<span style="color: #999999;">Nicht verknüpft</span>
|
||||||
|
% }
|
||||||
|
% elsif ($traewelling->{errored}) {
|
||||||
|
Fehlerhaft <i class="material-icons">error</i>
|
||||||
|
% }
|
||||||
|
% elsif (not ($traewelling->{push_sync} or $traewelling->{pull_sync})) {
|
||||||
|
<span style="color: #999999;">Verknüpft mit <%= $traewelling->{data}{user_name} // $traewelling->{email} %>, Synchronisierung inaktiv</span>
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
Verknüpft mit <%= $traewelling->{data}{user_name} // $traewelling->{email} %>
|
||||||
|
% }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Registriert am</th>
|
<th scope="row">Registriert am</th>
|
||||||
<td><%= $acc->{registered_at}->strftime('%d.%m.%Y %H:%M') %></td>
|
<td><%= $acc->{registered_at}->strftime('%d.%m.%Y %H:%M') %></td>
|
||||||
|
|
220
templates/traewelling.html.ep
Normal file
220
templates/traewelling.html.ep
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
% if (my $invalid = stash('invalid')) {
|
||||||
|
%= include '_invalid_input', invalid => $invalid
|
||||||
|
% }
|
||||||
|
|
||||||
|
<h1>Träwelling</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card purple">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<span class="card-title">Beta-Feature</span>
|
||||||
|
<p>Die Verbindung von Checkinservices bietet viele Möglichkeiten für interessante Fehlerbilder.
|
||||||
|
Falls etwas nicht klappt, bitte mit möglichst detaillierten Angaben zum Hergang einen Bug melden.</p>
|
||||||
|
<p>
|
||||||
|
Bekannte Probleme: Hooks werden bei einem Checkin via Träwelling nicht ausgelöst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<a href="https://github.com/derf/travelynx/issues" class="waves-effect waves-light btn-flat white-text">
|
||||||
|
<i class="material-icons left">bug_report</i>Bug melden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
% if (stash('new_traewelling')) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
% if ($traewelling->{token}) {
|
||||||
|
<div class="card success-color">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<span class="card-title">Träwelling verknüpft</span>
|
||||||
|
% my $user = $traewelling->{data}{user_name} // $traewelling->{email};
|
||||||
|
<p>Dein travelynx-Account hat nun ein Jahr lang Zugriff auf
|
||||||
|
den Träwelling-Account <b>@<%= $user %></b>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
% elsif (my $login_err = stash('login_error')) {
|
||||||
|
<div class="card caution-color">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<span class="card-title">Login-Fehler</span>
|
||||||
|
<p>Der Login bei Träwelling ist fehlgeschlagen: <%= $login_err %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
% elsif (my $logout_err = stash('logout_error')) {
|
||||||
|
<div class="card caution-color">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<span class="card-title">Logout-Fehler</span>
|
||||||
|
<p>Der Logout bei Träwelling ist fehlgeschlagen: <%= $logout_err %>.
|
||||||
|
Dein Login-Token bei travelynx wurde dennoch gelöscht, so
|
||||||
|
dass nun kein Zugriff von travelynx auf Träwelling mehr
|
||||||
|
möglich ist. In den <a
|
||||||
|
href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
|
||||||
|
kannst du ihn vollständig löschen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
|
||||||
|
% if (not $traewelling->{token}) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<p>
|
||||||
|
Hier hast du die Möglichkeit, deinen travelynx-Account mit einem
|
||||||
|
Account bei <a href="https://traewelling.de">Träwelling</a> zu
|
||||||
|
verknüpfen. Dies erlaubt die automatische Übernahme von Checkins
|
||||||
|
zwischen den beiden Diensten. Träwelling-Checkins in
|
||||||
|
Nahverkehrsmittel und Züge außerhalb des deutschen Schienennetzes
|
||||||
|
werden nicht unterstützt und ignoriert.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Mit E-Mail und Passwort wird ein Login über die Träwelling-API
|
||||||
|
durchgeführt. Die E-Mail und das dabei generierte Token werden
|
||||||
|
von travelynx gespeichert. Das Passwort wird ausschließlich für
|
||||||
|
den Login verwendet und nicht gespeichert. Der Login kann jederzeit
|
||||||
|
sowohl auf dieser Seite als auch über die <a
|
||||||
|
href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
|
||||||
|
widerrufen werden. Nach einem Jahr läuft er automatisch ab.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
%= form_for '/account/traewelling' => (method => 'POST') => begin
|
||||||
|
%= csrf_field
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">account_circle</i>
|
||||||
|
%= text_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
%= password_field 'password', id => 'password', class => 'validate', required => undef
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
</div>
|
||||||
|
<div class="col s12 center-align">
|
||||||
|
<button class="btn waves-effect waves-light" type="submit" name="action" value="login">
|
||||||
|
Verknüpfen
|
||||||
|
<i class="material-icons right">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
%= end
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<p>
|
||||||
|
Dieser travelynx-Account ist mit dem Träwelling-Account
|
||||||
|
% if (my $user = $traewelling->{data}{user_name}) {
|
||||||
|
<a href="https://traewelling.de/profile/<%= $user %>"><%= $user %></a>
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
%= $traewelling->{email}
|
||||||
|
% }
|
||||||
|
verknüpft.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%= form_for '/account/traewelling' => (method => 'POST') => begin
|
||||||
|
<div class="row">
|
||||||
|
%= csrf_field
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
%= radio_button sync_source => 'none'
|
||||||
|
<span>Keine Synchronisierung</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
%= radio_button sync_source => 'travelynx', disabled => undef
|
||||||
|
<span>Checkin-Synchronisierung travelynx → Träwelling</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p>Die Synchronisierung erfolgt spätestens drei Minuten nach der
|
||||||
|
Zielwahl. Träwelling-Checkins können von travelynx noch nicht
|
||||||
|
rückgängig gemacht werden.</p>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
%= radio_button sync_source => 'traewelling'
|
||||||
|
<span>Checkin-Synchronisierung Träwelling → travelynx</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p>Alle drei Minuten wird dein Status auf Träwelling abgefragt.
|
||||||
|
Falls du gerade in einen Zug eingecheckt bist, wird dieser von
|
||||||
|
travelynx übernommen. Träwelling-Checkins in Nahverkehrsmittel
|
||||||
|
und Züge außerhalb des deutschen Schienennetzes werden nicht
|
||||||
|
unterstützt.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row hide-on-small-only">
|
||||||
|
<div class="col s12 m6 l6 center-align">
|
||||||
|
<button class="btn waves-effect waves-light red" type="submit" name="action" value="logout">
|
||||||
|
Abmelden
|
||||||
|
<i class="material-icons right" aria-hidden="true">sync_disabled</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col s12 m6 l6 center-align">
|
||||||
|
<button class="btn waves-effect waves-light" type="submit" name="action" value="config">
|
||||||
|
Speichern
|
||||||
|
<i class="material-icons right" aria-hidden="true">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row hide-on-med-and-up">
|
||||||
|
<div class="col s12 m6 l6 center-align">
|
||||||
|
<button class="btn waves-effect waves-light" type="submit" name="action" value="config">
|
||||||
|
Speichern
|
||||||
|
<i class="material-icons right" aria-hidden="true">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col s12 m6 l6 center-align" style="margin-top: 1em;">
|
||||||
|
<button class="btn waves-effect waves-light red" type="submit" name="action" value="logout">
|
||||||
|
Abmelden
|
||||||
|
<i class="material-icons right" aria-hidden="true">sync_disabled</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%= end
|
||||||
|
<h2>Status</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12"">
|
||||||
|
% if ($traewelling->{latest_run}->epoch) {
|
||||||
|
Letzter Kontakt mit Träwelling <%= $traewelling->{latest_run}->strftime('am %d.%m.%Y um %H:%M:%S') %><br/>
|
||||||
|
% if ($traewelling->{errored}) {
|
||||||
|
<i class="material-icons left">error</i>
|
||||||
|
Fehler: <%= $traewelling->{data}{error} %>
|
||||||
|
% }
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
Bisher wurde noch keine Synchronisierung durchgeführt.
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>Log</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12"">
|
||||||
|
<ul>
|
||||||
|
% for my $log_entry (@{$traewelling->{data}{log} // []}) {
|
||||||
|
<li>
|
||||||
|
<%= $log_entry->[0]->strftime('%d.%m.%Y %H:%M:%S') %> –
|
||||||
|
% if ($log_entry->[2]) {
|
||||||
|
Träwelling <a href="https://traewelling.de/status/<%= $log_entry->[2] %>">#<%= $log_entry->[2] %></a> –
|
||||||
|
% }
|
||||||
|
%= $log_entry->[1]
|
||||||
|
</li>
|
||||||
|
% }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
Loading…
Reference in a new issue