File Coverage

blib/lib/Microsoft/Teams/WebHook.pm
Criterion Covered Total %
statement 95 103 92.2
branch 15 18 83.3
condition n/a
subroutine 17 18 94.4
pod 6 10 60.0
total 133 149 89.2


line stmt bran cond sub pod time code
1              
2             package Microsoft::Teams::WebHook 1.02;
3              
4 1     1   206694 use v5.26;
  1         3  
5 1     1   4 use warnings;
  1         1  
  1         44  
6 1     1   751 use Object::Pad;
  1         12298  
  1         2  
7              
8             # ABSTRACT: Microsoft Teams WebHook with AdaptiveCards for formatting notifications
9              
10             =encoding utf-8
11              
12             =head1 SYNOPSIS
13              
14             Sample usage to post notifications using Microsoft::Teams::WebHook
15              
16             =head1 DESCRIPTION
17              
18             Microsoft::Teams::WebHook
19              
20             Set of helpers to send plain or AdaptiveCard notifications
21              
22             =head1 Constructor attributes
23              
24             =head2 url [required]
25              
26             The backend C<url> for your Teams webhook
27              
28             =head2 json [optional]
29              
30             This is optional and allow you to provide an alternate JSON object
31             to format the output sent to post queries.
32              
33             One JSON::MaybeXS with the flavor of your choice.
34             By default C<utf8 = 0, pretty = 1>.
35              
36             =head2 auto_detect_utf8 [default=true] [optional]
37              
38             You can provide a boolean to automatically try to detect utf8 strings
39             and enable the utf8 flag.
40              
41             This is on by default but you can disable it by using
42              
43             my $hook = Slack::WebHook->new( ..., auto_detect_utf8 => 0 );
44              
45             =cut
46              
47             class Microsoft::Teams::WebHook {
48              
49 1     1   1510 use DateTime;
  1         749424  
  1         80  
50 1     1   980 use DateTime::Format::Human::Duration;
  1         4696  
  1         92  
51 1     1   768 use Encode;
  1         19960  
  1         148  
52 1     1   1203 use HTTP::Tiny;
  1         93738  
  1         88  
53 1     1   1248 use JSON::XS;
  1         12154  
  1         4833  
54              
55             field $url : param;
56             field $json : param = undef;
57             field $auto_detect_utf8 : param = 1;
58              
59             field $ua;
60             field $started_at;
61              
62             ADJUST {
63             $ua = HTTP::Tiny->new(
64             default_headers => {
65             'Content-Type' => 'application/json; charset=UTF-8'
66             }
67             );
68              
69             $json = JSON::XS->new->utf8(0)->pretty(1) unless (defined($json));
70             }
71              
72             #<<V perltidy can't handle Object::Pad's lexical methods
73             method $http_post($data) {
74             return $ua->post($url, {content => $json->encode($data)})
75             }
76             #>>V
77              
78 16     16 0 23 sub encode_text_values ($data) {
  16         20  
  16         21  
79 16         36 foreach my $field (qw{text title append}) {
80 48 100       110 if (defined($data->{$field})) {
81             Encode::_utf8_on($data->{$field})
82 17 100       95 unless (Encode::is_utf8($data->{$field}));
83             }
84             }
85             }
86              
87 13     13 0 25 sub merge_tpl (%params) {
  13         53  
  13         24  
88             my $body = [
89             map +{
90             type => 'TextBlock',
91             text => $_,
92             wrap => 1,
93             color => $params{text_color}
94             },
95             ref($params{text}) eq 'ARRAY' ? $params{text}->@* : $params{text}
96 13 100       101 ];
97              
98 13 50       29 if (defined($params{title})) {
99             unshift(
100             $body->@*, {
101             type => 'TextBlock',
102             text => $params{title},
103 0         0 weight => 'bolder',
104             size => 'medium',
105             wrap => 1,
106             style => 'heading'
107             }
108             );
109             }
110              
111 13 100       26 if (defined($params{append})) {
112             push(
113             $body->@*, {
114             type => 'RichTextBlock',
115             inlines => [
116             {
117             type => 'TextRun',
118             text => $params{append},
119 1         12 italic => 1,
120             }
121             ]
122             }
123             );
124             }
125              
126             return {
127 13         132 type => 'message',
128             attachments => [
129             {
130             contentType => "application/vnd.microsoft.card.adaptive",
131             contentUrl => undef,
132             content => {
133             '$schema' => 'http://adaptivecards.io/schemas/adaptive-card.json',
134             type => 'AdaptiveCard',
135             version => '1.5',
136             msteams => {
137             width => 'Full'
138             },
139             body => $body
140             }
141             }
142             ]
143             };
144             }
145              
146 2     2 1 1743 method post ($message) {
  2         6  
  2         4  
  2         2  
147 2 100       8 my $params = ref($message) eq 'HASH' ? $message : {text => $message};
148 2 50       10 encode_text_values($params) if ($auto_detect_utf8);
149 2         5 $self->$http_post($params);
150             }
151              
152             #<<V perltidy can't handle Object::Pad's lexical methods
153             method $get_params(@params) {
154             my %p = (@params == 1) ? (text => $params[0]) : @params;
155             encode_text_values(\%p) if($auto_detect_utf8);
156             return %p;
157             }
158             #>>V
159              
160 0     0 0 0 method post_msg ($message, @list) {
  0         0  
  0         0  
  0         0  
  0         0  
161 0         0 my %params = $self->$get_params($message, @list);
162 0         0 $self->$http_post(merge_tpl(%params));
163             }
164              
165 6     6 1 16099 method post_ok ($message, @list) {
  6         38  
  6         10  
  6         13  
  6         8  
166 6         17 my %params = $self->$get_params($message, @list);
167 6 100       20 $params{text_color} = 'good' unless (exists($params{text_color}));
168 6         18 $self->$http_post(merge_tpl(%params));
169             }
170              
171 2     2 1 6140 method post_warning ($message, @list) {
  2         8  
  2         4  
  2         3  
  2         3  
172 2         5 my %params = $self->$get_params($message, @list);
173 2         4 $params{text_color} = 'warning';
174 2         5 $self->$http_post(merge_tpl(%params));
175             }
176              
177 3     3 1 5929 method post_info ($message, @list) {
  3         10  
  3         6  
  3         3  
  3         3  
178 3         10 my %params = $self->$get_params($message, @list);
179 3         9 $params{text_color} = 'accent';
180 3         7 $self->$http_post(merge_tpl(%params));
181             }
182              
183 2     2 1 6050 method post_error ($message, @list) {
  2         9  
  2         4  
  2         5  
  2         2  
184 2         7 my %params = $self->$get_params($message, @list);
185 2         9 $params{text_color} = 'attention';
186 2         10 $self->$http_post(merge_tpl(%params));
187             }
188              
189 1     1 1 3344 method post_start ($message, @list) {
  1         4  
  1         2  
  1         2  
  1         1  
190 1         10 $started_at = DateTime->now();
191 1         495 $self->post_info($message, @list);
192             }
193              
194 1     1 0 3004384 method post_end ($message, @list) {
  1         15  
  1         5  
  1         4  
  1         3  
195 1         8 my %params = $self->$get_params($message, @list);
196 1         80 my %append;
197 1 50       6 if (defined($started_at)) {
198 1         12 my $dur = DateTime->now() - $started_at;
199 1         1241 %append = (append => 'run time: ' . DateTime::Format::Human::Duration->new()->format_duration($dur));
200 1         367 $started_at = undef;
201             }
202 1         10 $self->post_ok(%params, %append);
203             }
204              
205             }
206              
207             =head1 METHODS
208              
209             =head2 new( [url => "https://..." ] )
210              
211             This is the constructor for L<Microsoft::Teams::WebHook>. You should provide the C<url> for your webhook.
212             You should visit the L<official Microsoft documentation page|https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet>
213             for information on how to create this URL.
214              
215             =head2 post( $message )
216              
217             The L<post> method allows you to post a single message without applying any formatting.
218             The return value is the return of L<HTTP::Tiny::post> which is one C<Hash Ref>.
219             The C<success> field will be true if the status code is 2xx.
220              
221             The other C<post_*> methods format the message contents in L<AdaptiveCards|https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#send-adaptive-cards-using-an-incoming-webhook>
222             whereas this method allows you to send a simple, unformatted plaintext message
223             or provide a C<HashRef> structure (which will be converted to a JSON string) for
224             custom C<AdaptiveCards>.
225              
226             =head2 post_ok( $message, [ @list ])
227              
228             Post a message to the Teams WebHook URL. There are two methods of calling a C<post_*> method.
229              
230             You may either pass a simple string argument to the function
231              
232             Microsoft::Teams::WebHook->new(url => ...)->post_ok( q{posting a simple "ok" text} );
233              
234             or you can pass a hash (not a hashref!) with a required C<text> key
235              
236             Microsoft::Teams::WebHook->new(url => ...)->post_ok(text => q{your notification message});
237              
238             Using the latter form, you may also optionally add C<title> and C<text_color> (for C<post_ok> only!) keys
239             to set those additional parameters in the resulting AdaptiveCard. If C<text_color> is specified,
240             it must be one of the allowed AdaptiveCard colors (see L<documentation|https://adaptivecards.io/explorer/TextRun.html>)
241             which as of this writing are: C<default>, C<dark>, C<light>, C<accent>, C<good>, C<warning>, and C<attention>.
242              
243             Microsoft::Teams::WebHook->new(url => ...)->post_ok(
244             title => q{YOUR NOTIFICATION TITLE},
245             text => q{your notification message},
246             text_color => 'light'
247             );
248              
249             C<post_ok> defaults to C<good> text color if none is given.
250              
251             The return value of the C<post_*> method is one L<HTTP::Tiny> response, a C<HashRef>
252             containing the C<success> field, which is true on success.
253              
254             =head2 post_warning( $message, [ @list ])
255              
256             Similar to L<post_ok>, but the color used to display the message is C<warning> and
257             cannot be overridden.
258              
259             =head2 post_info( $message, [ @list ])
260              
261             Similar to L<post_ok>, but the color used to display the message is C<accent> and
262             cannot be overridden.
263              
264             =head2 post_error( $message, [ @list ])
265              
266             Similar to L<post_ok>, but the color used to display the message is C<attention> and
267             cannot be overridden.
268              
269             =head2 post_start( $message, [ @list ])
270              
271             Similar to L<post_ok>, but the color used to display the message is C<accent> and
272             cannot be overridden. Additionally, this method starts a timer, which is used by
273             L<post_end>.
274              
275             =head2 post_warning( $message, [ @list ])
276              
277             Similar to L<post_ok>, but the default color used to display the message is C<good>.
278             An additional italicized, C<default>-colored line is appended to the AdaptiveCard
279             which displays a human-readable format of the elapsed time since C<post_start>
280             was called.
281              
282             If C<post_start> was not previously called (or not called since the last C<post_end>)
283             the timer is considered to have no value and the elapsed time section will not
284             be added.
285              
286             =head1 CREDITS
287              
288             This module is heavily based on the excellent work done on L<Slack::WebHook>
289             (and as such intended to be largely drop-in compatible)
290              
291             =head1 AUTHOR
292              
293             Mark Tyrrell C<< <mark@tyrrminal.dev> >>
294              
295             =head1 LICENSE
296              
297             Copyright (c) 2024 Mark Tyrrell
298              
299             Permission is hereby granted, free of charge, to any person obtaining a copy
300             of this software and associated documentation files (the "Software"), to deal
301             in the Software without restriction, including without limitation the rights
302             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
303             copies of the Software, and to permit persons to whom the Software is
304             furnished to do so, subject to the following conditions:
305              
306             The above copyright notice and this permission notice shall be included in all
307             copies or substantial portions of the Software.
308              
309             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
310             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
311             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
312             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
313             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
314             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
315             SOFTWARE.
316              
317             =cut
318              
319             1;
320              
321             __END__