WiP webhook support

This commit is contained in:
Daniel Friesel 2019-05-05 18:09:11 +02:00
parent 55581d1f25
commit b36ba45aef
4 changed files with 225 additions and 0 deletions

View file

@ -347,6 +347,7 @@ sub startup {
"Checkin($uid): INSERT failed: $@"); "Checkin($uid): INSERT failed: $@");
return ( undef, 'INSERT failed: ' . $@ ); return ( undef, 'INSERT failed: ' . $@ );
} }
$self->run_hook( $self->current_user->{id}, 'checkin' );
return ( $train, undef ); return ( $train, undef );
} }
} }
@ -366,6 +367,7 @@ sub startup {
$self->app->log->error("Undo($uid, $journey_id): $@"); $self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@"; return "Undo($journey_id): $@";
} }
$self->run_hook( $uid, 'undo' );
return undef; return undef;
} }
if ( $journey_id !~ m{ ^ \d+ $ }x ) { if ( $journey_id !~ m{ ^ \d+ $ }x ) {
@ -421,6 +423,7 @@ sub startup {
$self->app->log->error("Undo($uid, $journey_id): $@"); $self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@"; return "Undo($journey_id): $@";
} }
$self->run_hook( $uid, 'undo' );
return undef; return undef;
} }
); );
@ -572,6 +575,7 @@ sub startup {
if ( $has_arrived or $force ) { if ( $has_arrived or $force ) {
return ( 0, undef ); return ( 0, undef );
$self->run_hook( $uid, 'checkout' );
} }
return ( 1, undef ); return ( 1, undef );
} }
@ -984,6 +988,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( $self->helper(
'get_user_password' => sub { 'get_user_password' => sub {
my ( $self, $name ) = @_; my ( $self, $name ) = @_;
@ -1753,6 +1864,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('/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');
$authed_r->get('/account/password')->to('account#password_form'); $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('/s/*station')->to('traveling#station');
$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('/journey/add')->to('traveling#add_journey_form'); $authed_r->post('/journey/add')->to('traveling#add_journey_form');
$authed_r->post('/journey/edit')->to('traveling#edit_journey'); $authed_r->post('/journey/edit')->to('traveling#edit_journey');
$authed_r->post('/account/password')->to('account#change_password'); $authed_r->post('/account/password')->to('account#change_password');

View file

@ -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 { sub setup_db {

View file

@ -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 { sub change_mail {
my ($self) = @_; my ($self) = @_;

View file

@ -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>