summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xlib/Travelynx.pm113
-rw-r--r--lib/Travelynx/Command/database.pm25
-rw-r--r--lib/Travelynx/Controller/Account.pm25
-rw-r--r--templates/webhooks.html.ep62
4 files changed, 225 insertions, 0 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 48fb22b..f058eee 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -347,6 +347,7 @@ sub startup {
"Checkin($uid): INSERT failed: $@");
return ( undef, 'INSERT failed: ' . $@ );
}
+ $self->run_hook( $self->current_user->{id}, 'checkin' );
return ( $train, undef );
}
}
@@ -366,6 +367,7 @@ sub startup {
$self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@";
}
+ $self->run_hook( $uid, 'undo' );
return undef;
}
if ( $journey_id !~ m{ ^ \d+ $ }x ) {
@@ -421,6 +423,7 @@ sub startup {
$self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@";
}
+ $self->run_hook( $uid, 'undo' );
return undef;
}
);
@@ -572,6 +575,7 @@ sub startup {
if ( $has_arrived or $force ) {
return ( 0, undef );
+ $self->run_hook( $uid, 'checkout' );
}
return ( 1, undef );
}
@@ -985,6 +989,113 @@ sub startup {
);
$self->helper(
+ 'get_webhook' => sub {
+ my ( $self, $uid ) = @_;
+ $uid //= $self->current_user->{id};
+
+ my $res_h
+ = $self->pg->db->select( 'webhooks_str', '*',
+ { user_id => $uid } )->hash;
+
+ $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} );
+
+ return $res_h;
+ }
+ );
+
+ $self->helper(
+ 'set_webhook' => sub {
+ my ( $self, %opt ) = @_;
+
+ $opt{uid} //= $self->current_user->{id};
+
+ my $res = $self->pg->db->insert(
+ 'webhooks',
+ {
+ user_id => $opt{uid},
+ enabled => $opt{enabled},
+ url => $opt{url},
+ token => $opt{token}
+ },
+ {
+ on_conflict => \
+'(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null'
+ }
+ );
+ }
+ );
+
+ $self->helper(
+ 'mark_hook_status' => sub {
+ my ( $self, $uid, $url, $success, $text ) = @_;
+
+ if ( length($text) > 1024 ) {
+ $text = "(output too long)";
+ }
+
+ $self->pg->db->update(
+ 'webhooks',
+ {
+ errored => !$success,
+ latest_run => DateTime->now( time_zone => 'Europe/Berlin' ),
+ output => $text,
+ },
+ {
+ user_id => $uid,
+ url => $url
+ }
+ );
+ }
+ );
+
+ $self->helper(
+ 'run_hook' => sub {
+ my ( $self, $uid, $reason ) = @_;
+
+ my $hook = $self->get_webhook($uid);
+
+ if ( not $hook->{enabled} or not $hook->{url} =~ m{^ https?:// }x )
+ {
+ return;
+ }
+
+ my $status = { todo => 1 };
+ my $header = {};
+ my $hook_body = {
+ reason => $reason,
+ status => $status,
+ };
+
+ if ( $hook->{token} ) {
+ $hook->{token} =~ tr{\r\n}{}d;
+ $header->{Authorization} = "Bearer $hook->{token}";
+ }
+
+ my $ua = $self->ua;
+ $ua->request_timeout(10);
+
+ $ua->post_p( $hook->{url} => $header => json => $hook_body )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ $self->mark_hook_status( $uid, $hook->{url}, 0,
+ "HTTP $err->{code} $err->{message}" );
+ }
+ else {
+ $self->mark_hook_status( $uid, $hook->{url}, 1,
+ $tx->result->body );
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->mark_hook_status( $uid, $hook->{url}, 0, $err );
+ }
+ )->wait;
+ }
+ );
+
+ $self->helper(
'get_user_password' => sub {
my ( $self, $name ) = @_;
@@ -1753,6 +1864,7 @@ sub startup {
$authed_r->get('/account')->to('account#account');
$authed_r->get('/account/privacy')->to('account#privacy');
+ $authed_r->get('/account/hooks')->to('account#webhook');
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
$authed_r->get('/cancelled')->to('traveling#cancelled');
$authed_r->get('/account/password')->to('account#password_form');
@@ -1767,6 +1879,7 @@ sub startup {
$authed_r->get('/s/*station')->to('traveling#station');
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
$authed_r->post('/account/privacy')->to('account#privacy');
+ $authed_r->post('/account/hooks')->to('account#webhook');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
$authed_r->post('/account/password')->to('account#change_password');
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index 79ff086..11a946e 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -456,6 +456,31 @@ my @migrations = (
}
);
},
+
+ # v10 -> v11
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table webhooks (
+ user_id integer not null references users (id) primary key,
+ enabled boolean not null,
+ url varchar(1000) not null,
+ token varchar(250),
+ errored boolean,
+ latest_run timestamptz,
+ output text
+ );
+ comment on table webhooks is 'URLs and bearer tokens for push events';
+ create view webhooks_str as select
+ user_id, enabled, url, token, errored, output,
+ extract(epoch from latest_run) as latest_run_ts
+ from webhooks
+ ;
+ update schema_version set version = 11;
+ }
+ );
+ },
);
sub setup_db {
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index 8d5b21f..75b8f02 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -230,6 +230,31 @@ sub privacy {
}
}
+sub webhook {
+ my ($self) = @_;
+
+ my $hook = $self->get_webhook;
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ $hook->{url} = $self->param('url');
+ $hook->{token} = $self->param('token');
+ $hook->{enabled} = $self->param('enabled') // 0;
+ $self->set_webhook(
+ url => $hook->{url},
+ token => $hook->{token},
+ enabled => $hook->{enabled}
+ );
+ $hook = $self->get_webhook;
+ }
+ else {
+ $self->param( url => $hook->{url} );
+ $self->param( token => $hook->{token} );
+ $self->param( enabled => $hook->{enabled} );
+ }
+
+ $self->render( 'webhooks', hook => $hook );
+}
+
sub change_mail {
my ($self) = @_;
diff --git a/templates/webhooks.html.ep b/templates/webhooks.html.ep
new file mode 100644
index 0000000..fec485d
--- /dev/null
+++ b/templates/webhooks.html.ep
@@ -0,0 +1,62 @@
+% if (my $invalid = stash('invalid')) {
+ %= include '_invalid_input', invalid => $invalid
+% }
+
+<h1>Web Hooks</h1>
+
+<!-- -H "Authorization: Bearer ${TOKEN}" -->
+<div class="row">
+ <div class="col s12">
+ <p>
+ Die im Web Hook konfigurierte URL wird bei jedem Checkin und Checkout
+ des ausgewählten Zuges aufgerufen. Falls ein Token eingetragen
+ ist, wird er als Bearer Token verwendet.
+ </p>
+ <p>
+ Events werden als JSON POST übertragen. Das JSON-Dokument besteht aus
+ zwei Feldern: „reason“ gibt den Grund des API-Aufrufs an (checkin,
+ checkout, undo), „status“ den <a href="/api">aktuellen Status</a>.
+ </p>
+ </div>
+ %= form_for '/account/hooks' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="col s12 center-align">
+ <label>
+ %= check_box enabled => 1
+ <span>Aktiv</span>
+ </label>
+ </div>
+ <div class="input-field col s12">
+ <i class="material-icons prefix">link</i>
+ %= text_field 'url', id => 'url', class => 'validate', maxlength => 1000
+ <label for="url">URL</label>
+ </div>
+ <div class="input-field col s12">
+ <i class="material-icons prefix">lock</i>
+ %= text_field 'token', id => 'token', class => 'validate', maxlength => 250
+ <label for="token">Token</label>
+ </div>
+ <div class="col s12">
+ % if ($hook->{latest_run}->epoch) {
+ Zuletzt ausgeführt: <%= $hook->{latest_run} %><br/>
+ % if ($hook->{errored}) {
+ <i class="material-icons left">error</i>
+ Status: <%= $hook->{output} %>
+ % }
+ % else {
+ <i class="material-icons left">check</i>
+ Server-Antwort: <%= $hook->{output} %>
+ % }
+ % }
+ % else {
+ Noch nicht ausgeführt.
+ % }
+ </div>
+ <div class="col s12 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
+ Speichern
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ %= end
+</div>