# Copyright (c) 2019, cPanel, LLC.
# All rights reserved.
# http://cpanel.net
#
# This is free software; you can redistribute it and/or modify it under the
# same terms as Perl itself. See L<perlartistic>.
package Slack::WebHook;
use strict;
use warnings;
# ABSTRACT: Slack WebHook with preset layout & colors for sending slack notifications
BEGIN {
our $VERSION = '0.003'; # VERSION: generated by DZP::OurPkgVersion
}
use Simple::Accessor qw{
url
json
auto_detect_utf8
_http
_started_at
};
use HTTP::Tiny;
use JSON::XS ();
use Encode ();
use constant SLACK_COLOR_START => '#2b3bd9'; # blue
use constant SLACK_COLOR_OK => '#2eb886'; # green
use constant SLACK_COLOR_INFO => '#4b42f4'; # blue
use constant SLACK_COLOR_ERROR => '#cc0000'; # red
use constant SLACK_COLOR_WARNING => '#f5ca46'; # yellowish
sub _build_url {
die q[Missing URL, please set it using Slack::WebHook->new( url => ... )];
}
sub _build__http {
return HTTP::Tiny->new(
default_headers => {
'Content-Type' => 'application/json; charset=UTF-8',
}
);
}
sub _build_json {
return JSON::XS->new->utf8(0)->pretty(1);
}
# by default on
sub _build_auto_detect_utf8 { 1 }
sub post {
my ( $self, @opts ) = @_;
die "post method only takes one argument: txt or hash ref"
if scalar @opts > 1;
my $txt_or_ref = $opts[0];
my $data = ref $txt_or_ref ? $txt_or_ref : { text => $txt_or_ref };
return $self->_http_post($data);
}
sub post_start {
my ( $self, @args ) = @_;
$self->_started_at( scalar time() );
return $self->_notify(
{ color => SLACK_COLOR_START },
@args
);
}
sub post_end {
my ( $self, @args ) = @_;
my $delta = int( ( time() - ( $self->_started_at() // 0 ) ) );
my $timestr = '';
my $hours = int( $delta / 3600 );
if ($hours) {
$delta -= $hours * 3600;
$timestr .= " " if length $timestr;
$timestr .= "$hours hour" . ( $hours > 1 ? 's' : '' );
}
my $minutes = int( $delta / 60 );
if ($minutes) {
$delta -= $minutes * 60;
$timestr .= " " if length $timestr;
$timestr .= "$minutes minute" . ( $minutes > 1 ? 's' : '' );
}
my $seconds = $delta;
{
$timestr .= " " if length $timestr;
$timestr .= "$seconds second" . ( $seconds > 1 ? 's' : '' );
}
return $self->_notify(
{
color => SLACK_COLOR_OK,
$delta > 0 ? ( post_text => "\n_run time: ${timestr}_" ) : ()
},
@args
);
}
sub post_ok {
my ( $self, @args ) = @_;
return $self->_notify(
{ color => SLACK_COLOR_OK },
@args
);
# could also work
#return $self->notify_slack( @args );
}
sub post_warning {
my ( $self, @args ) = @_;
return $self->_notify(
{ color => SLACK_COLOR_WARNING },
@args
);
}
sub post_error {
my ( $self, @args ) = @_;
return $self->_notify(
{ color => SLACK_COLOR_ERROR },
@args
);
}
sub post_info {
my ( $self, @args ) = @_;
return $self->_notify(
{ color => SLACK_COLOR_INFO },
@args
);
}
sub _notify {
my ( $self, $defaults, @args ) = @_;
die("defaults should be a Hash Ref") unless ref $defaults eq 'HASH';
my @user;
my $nb_args = scalar @args;
if ( $nb_args <= 1 ) {
@user = ( text => $args[0] );
}
else {
if ( $nb_args % 2 ) {
my (@caller) = caller(1);
my $called_by = $caller[3] // 'unknown';
die q[Invalid number of args from $called_by];
}
@user = @args;
}
return $self->notify_slack( %$defaults, @user );
}
sub notify_slack {
my ( $self, @args ) = @_;
my ( $color, $title, $text );
my $nb_args = scalar @args;
die q[No arguments to notify_slack] unless $nb_args;
if ( $nb_args == 1 ) {
$text = $args[0];
}
else {
die q[Invalid number of arguments] if $nb_args % 2;
my %opts = @args;
$color = $opts{color};
$title = $opts{title}; # can be undefined
$text = $opts{text} // $opts{body} // $opts{content} // '';
$text .= $opts{post_text} if defined $opts{post_text};
}
# title can be undefined
$color //= SLACK_COLOR_OK;
$text //= '';
my $data = {
attachments => [
{
defined $title ? ( title => $title ) : (),
color => $color,
text => $text,
mrkdwn_in => [qw/text title/],
}
]
};
return $self->_http_post($data);
}
sub _http_post {
my ( $self, $data ) = @_;
die unless ref $data eq 'HASH';
if ( $self->auto_detect_utf8 ) {
foreach my $field (qw{text title post_text}) {
if ( defined $data->{$field} ) {
if ( !Encode::is_utf8( $data->{$field} ) ) {
Encode::_utf8_on( $data->{$field} );
}
}
}
}
return $self->_http->post_form(
$self->url,
{ payload => $self->json->encode($data) },
);
}
1;
__END__
=pod
=encoding utf-8
=head1 NAME
Slack::WebHook - Slack WebHook with preset layout & colors for sending slack notifications
=head1 VERSION
version 0.003
=head1 SYNOPSIS
Sample usage to post slack notifications using Slack::WebHook
#!perl
use Slack::WebHook ();
my $hook = Slack::WebHook->new(
url => 'https://hooks.slack.com/services/xxxx/xxxx...'
);
# using some preset decorations with markdown syntax enabled
$hook->post_ok( 'a pretty _green_ message' );
$hook->post_warning( 'a pretty _orange_ message' );
$hook->post_error( 'a pretty _red_ message' );
$hook->post_info( 'a pretty _blue_ message' );
# this is similar to the previous syntax
$hook->post_ok( text => 'a pretty _green_ message' );
# you can also set a title and a body to your message
# with any of the post_* methods
$hook->post_ok( # or any other post_* method
title => ':camel: My Title',
text => qq[A multiline\ncontent as an example],
);
# you can also set your own color if you want
$hook->post_info( # or any other post_* method
color => '#00cc00',
text => q[Hello, World! in green],
);
{
# using timers for your tasks
$hook->post_start( 'starting some task' );
sleep( 1 * 3600 + 12 * 60 + 45 ) if 0; # 1 hour 12 minutes 45 seconds
$hook->post_end( 'task is now finished' );
# automatically adds the run time at the end of your message:
# "\nrun time: 1 hour 12 minutes 45 seconds"
}
# using a custom color to a notification
$hook->post_end( text => 'task is now finished in black', color => '#000' );
# you can also post your own custom message without any preset styles
# this allow you to bypass the custom layout and provide your own hash struct
# which will be converted to JSON before posting the message
$hook->post( 'posting a raw message' );
$hook->post( { text => 'Hello, World!'} );
=head1 DESCRIPTION
Slack::WebHook
Set of helpers to send slack notification with preset decorations.
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-ok-camel.png" width="400" alt="Slack::WebHook chart"/></p>
=head1 Constructor attributes
=head2 url [required]
The backend C<url> for your Slack webhook.
=head2 json [optional]
This is optional and allow you to provide an alternate JSON object
to format the output sent to post queries.
One JSON::MaybeXS with the flavor of your choice.
By default C<utf8 = 0, pretty = 1>.
Example:
my $json = Cpanel::JSON::XS->new->utf8(0)->pretty->allow_nonref;
my $hook = Slack::WebHook->new(
json => $json,
url => ...
);
=head2 auto_detect_utf8 [default=true] [optional]
You can provide a boolean to automatically try to detect utf8 strings
and enable the utf8 flag.
This is on by default but you can disable it by using
my $hook = Slack::WebHook->new( ..., auto_detect_utf8 => 0 );
=head1 Available functions / methods
=head2 new( [ url => "https://..." ] )
This is the constructor for L<Slack::WebHook>. You should provide the C<url> for your webhook.
You should visit the L<official Slack documentation page|https://api.slack.com/slack-apps> to create your webhook
and get your personal URL.
=head2 post( $message )
The L<post> method allow you to post a single message without any preset decorations.
The return value is the return of L<HTTP::Tiny::post_form> which is one C<Hash Ref>.
The C<success> field will be true if the status code is 2xx.
You should prefer using any of the other methods C<post_*> which will use colors
and a preset style to display your notification.
The C<post> method allow you to post custom messages by bypassing any preset layour.
=head2 post_ok( $message, [ @list ] )
L<post_ok> submit a POST request to the Http URL set when constructing a L<Slack::WebHook> object.
You have two ways of calling a C<post_*> method.
Either you can simply pass a single string argument to the function
Slack::WebHook->new( URL => ... )->post_ok( q[posting a simple "ok" text] );
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-ok-text.png" width="400" alt="post_ok chart"/></p>
or you can also set an optional title or change the default color used for the notification
Slack::WebHook->new( URL => ... )
->post_ok(
title => ":camel: Notification Title",
text => "your notification message using _markdown_",
#color => '#aabbcc',
);
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-ok-camel.png" width="400" alt="Slack::WebHook chart"/></p>
The return value of the method C<post_*> is one L<HTTP::Tiny> reply. One C<Hash Ref> containing
the C<success> field which is true on success.
=head2 post_warning( $message, [ @list ] )
Similar to L<post_ok> but the color used to display the message is C<yellow>.
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-warning.png" width="400" alt="post_warning chart"/></p>
=head2 post_info( $message, [ @list ] )
Similar to L<post_ok> but the color used to display the message is C<blue>.
=head2 post_error( $message, [ @list ] )
Similar to L<post_ok> but the color used to display the message is C<red>.
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-error.png" width="400" alt="post_error chart"/></p>
=head2 post_start( $message, [ @list ] )
Similar to L<post_ok> but in addition initialize a timer which is used by L<post_stop>.
The default color used to display the message is C<blue>.
=head2 post_end( $message, [ @list ] )
The L<post_end> method should be used after calling L<post_start>.
This would convert the time elapsed between the two calls to a string appended at the end
of your message.
The default notification color is C<green>.
my $hook = Slack::WebHook->new( url => 'https://...' );
# simple start / end
$hook->post_start( 'starting some task' );
sleep( 1 * 3600 + 12 * 60 + 45 ); # 1 hour 12 minutes and 45 seconds
$hook->post_end( 'task is now finished' );
# using start / end with title and custom color
$hook->post_start( title => "Starting Task 42", text => "description..." );
sleep( 18 );
$hook->post_end( title => "Task 42 is now finished", color => "#000", text => 'task is now finished' );
=for HTML <p><img src="https://raw.githubusercontent.com/atoomic/Slack-WebHook/master/static/images/slack-start-stop.png" width="400" alt="post_start post_end chart"/></p>
=head1 Customize notifications colors
Using any of the C<post_*> methods: L<post_ok>, L<post_warning>, L<post_error>, L<post_info>, L<post_start>
or L<post_end> you can set an alternate color to use for your Slack notification.
my $webhook = Slack::WebHook->new( url => '...' );
# message without a title using a custom color
$webhook->post_ok( { text => 'Hello World! in black', color => '#000' );
# message with a title using a custom color
$webhook->post_warning( { title => 'My Title', text => 'Hello World! in red', color => '#cc0000' );
=head1 SEE ALSO
Please also consider the following modules:
=over
=item L<Slack::Notify> - powerful client for Slack webhooks which gives you a full control on the message layout
=back
=head1 TODO
=over
=item improve doc & add some extra examples
=item markdown on demand?
=item simplify return value from post_* methods?
=back
=head1 LICENSE
This software is copyright (c) 2019 by cPanel, L.L.C.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming
language system itself.
=head1 DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY
WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
=head1 AUTHOR
Nicolas R <atoomic@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2019 by cPanel, Inc.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut