WiP webhook support
This commit is contained in:
parent
55581d1f25
commit
b36ba45aef
4 changed files with 225 additions and 0 deletions
113
lib/Travelynx.pm
113
lib/Travelynx.pm
|
@ -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');
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) = @_;
|
||||||
|
|
||||||
|
|
62
templates/webhooks.html.ep
Normal file
62
templates/webhooks.html.ep
Normal 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>
|
Loading…
Reference in a new issue