File Coverage

blib/lib/Web/Mention.pm
Criterion Covered Total %
statement 250 264 94.7
branch 79 106 74.5
condition 15 21 71.4
subroutine 40 42 95.2
pod 5 7 71.4
total 389 440 88.4


line stmt bran cond sub pod time code
1             package Web::Mention;
2              
3 7     7   8889 use Moo;
  7         66189  
  7         33  
4 7     7   12282 use MooX::ClassAttribute;
  7         111902  
  7         41  
5 7     7   3732 use MooX::Enumeration;
  7         16017  
  7         28  
6 7     7   4484 use Types::Standard qw(InstanceOf Maybe Str Bool Num Enum);
  7         462266  
  7         73  
7 7     7   11504 use LWP;
  7         203726  
  7         243  
8 7     7   2919 use HTTP::Link;
  7         59025  
  7         326  
9 7     7   5474 use DateTime;
  7         3097206  
  7         402  
10 7     7   3829 use String::Truncate qw(elide);
  7         77618  
  7         66  
11 7     7   1725 use Try::Tiny;
  7         16  
  7         401  
12 7     7   69 use Types::Standard qw(Enum);
  7         93  
  7         175  
13 7     7   4555 use Scalar::Util qw(blessed);
  7         20  
  7         319  
14 7     7   65 use Carp qw(carp croak);
  7         15  
  7         283  
15 7     7   3969 use Mojo::DOM58;
  7         193734  
  7         320  
16 7     7   60 use URI::Escape;
  7         12  
  7         399  
17 7     7   3962 use Encode qw(decode_utf8);
  7         69729  
  7         524  
18 7     7   3443 use Readonly;
  7         26220  
  7         346  
19 7     7   3775 use DateTime::Format::ISO8601;
  7         587686  
  7         379  
20              
21 7     7   4488 use Web::Microformats2::Parser;
  7         1061620  
  7         253  
22 7     7   3209 use Web::Mention::Author;
  7         21  
  7         20484  
23              
24             our $VERSION = '0.712';
25              
26             Readonly my @VALID_RSVP_TYPES => qw(yes no maybe interested);
27              
28             has 'source' => (
29             isa => InstanceOf['URI'],
30             is => 'ro',
31             required => 1,
32             coerce => sub { URI->new($_[0]) },
33             );
34              
35             has 'original_source' => (
36             isa => InstanceOf['URI'],
37             is => 'lazy',
38             coerce => sub { URI->new($_[0]) },
39             clearer => '_clear_original_source',
40             );
41              
42             has 'source_html' => (
43             isa => Maybe[Str],
44             is => 'rw',
45             );
46              
47             has 'source_mf2_document' => (
48             isa => Maybe[InstanceOf['Web::Microformats2::Document']],
49             is => 'rw',
50             lazy => 1,
51             builder => '_build_source_mf2_document',
52             clearer => '_clear_mf2',
53             );
54              
55             has 'target' => (
56             isa => InstanceOf['URI'],
57             is => 'ro',
58             required => 1,
59             coerce => sub { URI->new($_[0]) },
60             );
61              
62             has 'endpoint' => (
63             isa => Maybe[InstanceOf['URI']],
64             is => 'lazy',
65             );
66              
67             has 'is_tested' => (
68             isa => Bool,
69             is => 'rw',
70             default => 0,
71             );
72              
73             has 'is_verified' => (
74             isa => Bool,
75             is => 'lazy',
76             );
77              
78             has 'time_verified' => (
79             isa => InstanceOf['DateTime'],
80             is => 'rw',
81             );
82              
83             has 'time_received' => (
84             isa => InstanceOf['DateTime'],
85             is => 'ro',
86             default => sub{ DateTime->now },
87             );
88              
89             has 'time_published' => (
90             isa => InstanceOf['DateTime'],
91             is => 'rw',
92             lazy => 1,
93             builder => '_build_time_published',
94             );
95              
96             has 'rsvp_type' => (
97             isa => Maybe[Str],
98             is => 'lazy',
99             );
100              
101             has 'author' => (
102             isa => Maybe[InstanceOf['Web::Mention::Author']],
103             is => 'lazy',
104             clearer => '_clear_author',
105             );
106              
107             has 'type' => (
108             isa => Maybe[Enum[qw(rsvp reply like repost quotation mention)]],
109             traits => ['Enumeration'],
110             handles => [qw(is_rsvp is_reply is_like is_repost is_quotation is_mention)],
111             is => 'lazy',
112             clearer => '_clear_type',
113             );
114              
115             has 'content' => (
116             isa => Maybe[Str],
117             is => 'lazy',
118             clearer => '_clear_content',
119             );
120              
121             has 'title' => (
122             isa => Maybe[Str],
123             is => 'lazy',
124             clearer => '_clear_title',
125             );
126              
127             has 'response' => (
128             isa => Maybe[InstanceOf['HTTP::Response']],
129             is => 'rw',
130             clearer => '_clear_response',
131             );
132              
133             class_has 'ua' => (
134             isa => InstanceOf['LWP::UserAgent'],
135             is => 'rw',
136             default => sub {
137             # Set the user-agent string to e.g. "Web::Mention/0.711"
138             LWP::UserAgent->new( agent => "$_[0]/$VERSION" );
139             },
140             );
141              
142             class_has 'max_content_length' => (
143             isa => Num,
144             is => 'rw',
145             default => 280,
146             );
147              
148             class_has 'content_truncation_marker' => (
149             isa => Str,
150             is => 'rw',
151             default => '...',
152             );
153              
154             sub _build_is_verified {
155 24     24   741 my $self = shift;
156              
157 24         62 return $self->verify;
158             }
159              
160             sub BUILD {
161 37     37 0 12379 my $self = shift;
162              
163 37         231 my $source = $self->source->clone;
164 37         641 my $target = $self->target->clone;
165              
166 37         541 foreach ( $source, $target ) {
167 74         510 $_->fragment( undef );
168             }
169              
170 37 100       409 if ( $source->eq( $target ) ) {
171 2         240 die "Inavlid webmention; source and target have the same URL "
172             . "($source)\n";
173             }
174             }
175              
176             sub new_from_request {
177 1     1 1 571 my $class = shift;
178              
179 1         4 my ( $request ) = @_;
180              
181 1 50 33     20 unless ( blessed($request) && $request->can('param') ) {
182 0         0 croak 'The argument to new_from_request must be an object that '
183             . "supports a param() method. (Got: $request)\n";
184             }
185              
186 1         4 my @complaints;
187             my %new_args;
188 1         3 foreach ( qw(source target) ) {
189 2 50       17 if ( my $value = $request->param( $_ ) ) {
190 2         23 $new_args{ $_ } = $value;
191             }
192              
193 2 50       8 unless ( defined $new_args{ $_ } ) {
194 0         0 push @complaints, "No param value set for $_.";
195             }
196             }
197              
198 1 50       7 if ( @complaints ) {
199 0         0 croak join q{ }, @complaints;
200             }
201              
202 1         32 return $class->new( %new_args );
203             }
204              
205             sub new_from_html {
206 3     3 1 3297 my $class = shift;
207              
208 3         15 my %args = @_;
209 3         10 my $source = $args{ source };
210 3         9 my $html = $args{ html };
211              
212 3 50       13 unless ($source) {
213 0         0 croak "You must define a source URL when calling new_from_html.";
214             }
215              
216 3         7 my @webmentions;
217              
218 3         29 my $dom = Mojo::DOM58->new( $html );
219 3         5236 my $nodes_ref = $dom->find( 'a[href]' );
220 3         2847 for my $node ( @$nodes_ref ) {
221 10         383 push @webmentions,
222             $class->new( source => $source, target => $node->attr( 'href' ) );
223             }
224              
225 3         336 return @webmentions;
226             }
227              
228              
229             sub verify {
230 24     24 1 44 my $self = shift;
231              
232 24         412 $self->is_tested(1);
233 24         1017 my $response = $self->ua->get( $self->source );
234              
235             # Search for both plain and escaped ("percent-encoded") versions of the
236             # target URL in the source doc. We search for the latter to account for
237             # sites like Tumblr, who treat outgoing hyperlinks as weird internally-
238             # pointing links that pass external URLs as query-string parameters.
239 24         199827 my $target = "$self->target";
240 24 100 100     110 if ( ($response->content =~ $self->target)
241             || ($response->content =~ uri_escape( $self->target ) )
242             ) {
243 22         975 $self->time_verified( DateTime->now );
244 22         7530 $self->source_html( $response->decoded_content );
245 22         40012 $self->_clear_mf2;
246 22         454 $self->_clear_content;
247 22         413 $self->_clear_title;
248 22         417 $self->_clear_author;
249 22         409 $self->_clear_type;
250 22         530 return 1;
251             }
252             else {
253 2         192 return 0;
254             }
255             }
256              
257             sub send {
258 8     8 1 1373 my $self = shift;
259              
260 8         128 my $endpoint = $self->endpoint;
261 8         174 my $source = $self->source;
262 8         12 my $target = $self->target;
263              
264 8 100       19 unless ( $endpoint ) {
265 3         90 $self->_clear_response;
266 3         23 return 0;
267             }
268              
269             # Step three: send the webmention to the target!
270 5         61 my $request = HTTP::Request->new( POST => $endpoint );
271 5         250 $request->content_type('application/x-www-form-urlencoded');
272 5         91 $request->content("source=$source&target=$target");
273              
274 5         193 my $response = $self->ua->request($request);
275 5         3852 $self->response( $response );
276              
277 5         124 return $response->is_success;
278             }
279              
280             sub _build_source_mf2_document {
281 19     19   155 my $self = shift;
282              
283 19 50       287 return unless $self->is_verified;
284 19         430 my $doc;
285             try {
286 19     19   1369 my $parser = Web::Microformats2::Parser->new;
287 19         585 $doc = $parser->parse(
288             $self->source_html,
289             url_context => $self->source,
290             );
291             }
292             catch {
293 0     0   0 die "Error parsing source HTML: $_";
294 19         170 };
295 19         279915 return $doc;
296             }
297              
298             sub _build_author {
299 0     0   0 my $self = shift;
300              
301 0 0       0 if ( $self->source_mf2_document ) {
302 0         0 return Web::Mention::Author->new_from_mf2_document(
303             $self->source_mf2_document
304             );
305             }
306             else {
307 0         0 return;
308             }
309             }
310              
311             sub _build_type {
312 9     9   8941 my $self = shift;
313              
314 9 50       151 unless ( $self->source_mf2_document ) {
315 0         0 return 'mention';
316             }
317              
318 9         456 my $item = $self->source_mf2_document->get_first( 'h-entry' );
319 9 50       1436 return 'mention' unless $item;
320              
321             # This order comes from the W3C Post Type Detection algorithm:
322             # https://www.w3.org/TR/post-type-discovery/#response-algorithm
323             # ...except adding 'quotation' as a final allowed type, before
324             # defaulting to 'mention'.
325              
326 9 100 66     208 if ( $self->rsvp_type
    100          
    100          
    100          
    100          
327             && $self->_check_url_property( $item, 'in-reply-to' ) ) {
328 1         19 return 'rsvp';
329             }
330             elsif ( $self->_check_url_property( $item, 'repost-of' )) {
331 1         22 return 'repost';
332             }
333             elsif ( $self->_check_url_property( $item, 'like-of' ) ) {
334 2         40 return 'like';
335             }
336             elsif ( $self->_check_url_property( $item, 'in-reply-to' ) ) {
337 2         49 return 'reply';
338             }
339             elsif ( $self->_check_url_property( $item, 'quotation-of' )) {
340 1         19 return 'quotation';
341             }
342             else {
343 2         39 return 'mention';
344             }
345             }
346              
347             sub _build_content {
348 9     9   281 my $self = shift;
349              
350             # If the source page has MF2 data *and* an h-entry,
351             # then we apply the algorithm outlined at:
352             # https://indieweb.org/comments#How_to_display
353             #
354             # Otherwise, we can't extract any semantic information about it,
355             # so we'll just offer the page's title, if there is one.
356              
357 9         14 my $item;
358 9 50       114 if ( $self->source_mf2_document ) {
359 9         319 $item = $self->source_mf2_document->get_first( 'h-entry' );
360             }
361              
362 9 100       1113 unless ( $item ) {
363 1         4 return $self->_title_element_content;
364             }
365              
366 8         13 my $raw_content;
367 8 100       19 if ( $item->get_property( 'content' ) ) {
368 6         80 $raw_content = $item->get_property( 'content' )->{ value };
369             }
370 8 100       87 if ( defined $raw_content ) {
371 6 100       114 if ( length $raw_content <= $self->max_content_length ) {
372 1         19 return $raw_content;
373             }
374             }
375              
376 7 100       40 if ( my $summary = $item->get_property( 'summary' ) ) {
377 3         33 return $self->_truncate_content( $summary );
378             }
379              
380 4 100       76 if ( defined $raw_content ) {
381 2         22 return $self->_truncate_content( $raw_content );
382             }
383              
384 2 50       5 if ( my $name = $item->get_property( 'name' ) ) {
385 2         22 return $self->_truncate_content( $name );
386             }
387              
388 0         0 return $self->_truncate_content( $item->value );
389             }
390              
391             sub _build_rsvp_type {
392 9     9   102 my $self = shift;
393              
394 9         19 my $rsvp_type;
395 9 50       138 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
396 9 100       1215 if ( my $rsvp_property = $item->get_property( 'rsvp' ) ) {
397 1 50       23 if ( grep { $_ eq lc $rsvp_property } @VALID_RSVP_TYPES ) {
  4         33  
398 1         7 $rsvp_type = $rsvp_property;
399             }
400             }
401             }
402              
403 9         266 return $rsvp_type;
404             }
405              
406             sub _check_url_property {
407 24     24   288 my $self = shift;
408 24         46 my ( $item, $property ) = @_;
409              
410 24         85 my $urls_ref = $item->get_properties( $property );
411 24         141 my $found = 0;
412              
413 24         46 for my $url_prop ( @$urls_ref ) {
414 28         67 my $url;
415 28 100 66     88 if ( blessed($url_prop) && $url_prop->isa('Web::Microformats2::Item') ) {
416 3         58 $url = $url_prop->value;
417             }
418             else {
419 25         40 $url = $url_prop;
420             }
421              
422 28 100       114 if ( $url eq $self->target ) {
423 7         36 $found = 1;
424 7         14 last;
425             }
426             }
427              
428 24         143 return $found;
429             }
430              
431             sub _truncate_content {
432 8     8   13 my $self = shift;
433 8         57 my ( $content ) = @_;
434 8 50       19 unless ( defined $content ) {
435 0         0 $content = q{};
436             }
437              
438 8         119 return elide(
439             $content,
440             $self->max_content_length,
441             {
442             at_space => 1,
443             marker => $self->content_truncation_marker,
444             },
445             );
446             }
447              
448             sub _build_original_source {
449 2     2   128 my $self = shift;
450              
451 2 50       37 if ( $self->source_mf2_document ) {
452 2 100       103 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
453 1 50       183 if ( my $url = $item->get_property( 'url' ) ) {
454 1         24 return $url;
455             }
456             }
457             }
458              
459 1         128 return $self->source;
460             }
461              
462             sub _build_time_published {
463 2     2   1707 my $self = shift;
464              
465 2 50       36 if ( $self->source_mf2_document ) {
466 2 100       46 if ( my $item = $self->source_mf2_document->get_first( 'h-entry' ) ) {
467 1 50       153 if ( my $time = $item->get_property( 'published' ) ) {
468 1         16 my $dt;
469             try {
470 1     1   56 $dt = DateTime::Format::ISO8601->parse_datetime( $time );
471 1         40 };
472 1 50       1044 return $dt if $dt;
473             }
474             }
475             }
476              
477 1         94 return $self->time_received;
478             }
479              
480             sub _build_endpoint {
481 8     8   58 my $self = shift;
482              
483 8         8 my $endpoint;
484 8         14 my $source = $self->source;
485 8         11 my $target = $self->target;
486              
487             # Is it in the Link HTTP header?
488 8         100 my $response = $self->ua->get( $target );
489 8 100       12572 if ( $response->header( 'Link' ) ) {
490 2         79 my @header_links = HTTP::Link->parse( $response->header( 'Link' ) . '' );
491 2         443 foreach (@header_links ) {
492 2         4 my $relation = $_->{relation};
493 2 100 66     11 if ($relation && $relation eq 'webmention') {
494 1         3 $endpoint = $_->{iri};
495             }
496             }
497             }
498              
499             # Is it in the HTML?
500 8 100       272 unless ( $endpoint ) {
501 7 100       18 if ( $response->header( 'Content-type' ) =~ m{^text/html\b} ) {
502 6         214 my $dom = Mojo::DOM58->new( $response->decoded_content );
503 6         7847 my $nodes_ref = $dom->find(
504             'link[rel~="webmention"], a[rel~="webmention"]'
505             );
506 6         2561 for my $node (@$nodes_ref) {
507 5         13 $endpoint = $node->attr( 'href' );
508 5 50       101 last if defined $endpoint;
509             }
510             }
511             }
512              
513 8 100       85 return undef unless defined $endpoint;
514              
515 6         18 $endpoint = URI->new_abs( $endpoint, $response->base );
516              
517 6         2549 my $host = $endpoint->host;
518 6 100 66     148 if (
519             ( lc($host) eq 'localhost' ) || ( $host =~ /^127\.\d+\.\d+\.\d+$/ )
520             ) {
521 1         6 carp "Warning: $source declares an apparent loopback address "
522             . "($endpoint) as a webmention endpoint. Ignoring.";
523 1         548 return undef;
524             }
525             else {
526 5         91 return $endpoint;
527             }
528             }
529              
530             sub _build_title {
531 3     3   3177 my $self = shift;
532              
533             # If the source doc has an h-entry with a p-name, return that, truncated.
534 3 50       44 if ( $self->source_mf2_document ) {
535 3         94 my $entry = $self->source_mf2_document->get_first( 'h-entry' );
536 3         315 my $name;
537 3 100       12 if ( $entry ) {
538 2         5 $name = $entry->get_property( 'name' );
539             }
540 3 100 100     34 if ( $entry && $name ) {
541 1         4 return $self->_truncate_content( $name );
542             }
543             }
544              
545             # Otherwise, try to return the HTML title element's content.
546 2         5 return $self->_title_element_content;
547              
548             }
549              
550             sub _title_element_content {
551 3     3   5 my $self = shift;
552              
553 3         43 my $title = Mojo::DOM58->new( $self->source_html )->at('title');
554 3 50       3329 if ($title) {
555 3         21 return $title->text;
556             }
557             else {
558 0         0 return undef;
559             }
560             }
561              
562             # Called by the JSON module during JSON encoding.
563             # Contrary to the (required) name, returns an unblessed reference, not JSON.
564             # See https://metacpan.org/pod/JSON#OBJECT-SERIALISATION
565             sub TO_JSON {
566 1     1 0 866 my $self = shift;
567              
568 1         11 my $return_ref = {
569             source => $self->source->as_string,
570             target => $self->target->as_string,
571             time_received => $self->time_received->epoch,
572             };
573              
574 1 50       43 if ( $self->is_tested ) {
575 1         19 $return_ref->{ is_tested } = $self->is_tested;
576 1         17 $return_ref->{ is_verified } = $self->is_verified;
577 1         20 $return_ref->{ type } = $self->type;
578 1         64 $return_ref->{ time_verified } = $self->time_verified->epoch;
579 1         35 $return_ref->{ content } = $self->content;
580 1         46 $return_ref->{ source_html } = $self->source_html;
581 1 50       17 if ( $self->source_mf2_document ) {
582             $return_ref->{ mf2_document_json } =
583 1         24 decode_utf8($self->source_mf2_document->as_json);
584             }
585             else {
586 0         0 $return_ref->{ mf2_document_json } = undef;
587             }
588             }
589              
590 1         240 return $return_ref;
591             }
592              
593             # Class method to construct a Webmention object from an unblessed reference,
594             # as created from the TO_JSON method. All-caps-named for the sake of parity.
595             sub FROM_JSON {
596 1     1 1 29 my $class = shift;
597 1         3 my ( $data_ref ) = @_;
598              
599 1         3 foreach ( qw( time_received time_verified ) ) {
600 2 50       259 if ( defined $data_ref->{ $_ } ) {
601             $data_ref->{ $_ } =
602 2         9 DateTime->from_epoch( epoch => $data_ref->{ $_ } );
603             }
604             }
605              
606 1         223 my $webmention = $class->new( $data_ref );
607              
608 1 50       15 if ( my $mf2_json = $data_ref->{ mf2_document_json } ) {
609 1         5 my $doc = Web::Microformats2::Document->new_from_json( $mf2_json );
610 1         876 $webmention->source_mf2_document( $doc );
611             }
612              
613 1         33 return $webmention;
614             }
615              
616             1;
617              
618             =pod
619              
620             =encoding UTF-8
621              
622             =head1 NAME
623              
624             Web::Mention - Implementation of the IndieWeb Webmention protocol
625              
626             =head1 SYNOPSIS
627              
628             use Web::Mention;
629             use Try::Tiny;
630             use v5.10;
631              
632             # Building a webmention from an incoming web request:
633              
634             my $wm;
635             try {
636             # $request can be any object that provides a 'param' method, such as
637             # Catalyst::Request or Mojo::Message::Request.
638             $wm = Web::Mention->new_from_request ( $request )
639             }
640             catch {
641             say "Oops, this wasn't a webmention at all: $_";
642             };
643              
644             if ( $wm && $wm->is_verified ) {
645             my $source = $wm->original_source;
646             my $target = $wm->target;
647             my $author = $wm->author;
648              
649             my $name;
650             if ( $author ) {
651             $name = $author->name;
652             }
653             else {
654             $name = $wm->source->host;
655             }
656              
657             if ( $wm->is_like ) {
658             say "Hooray, $name likes $target!";
659             }
660             elsif ( $wm->is_repost ) {
661             say "Gadzooks, over at $source, $name reposted $target!";
662             }
663             elsif ( $wm->is_reply ) {
664             say "Hmm, over at $source, $name said this about $target:";
665             say $wm->content;
666             }
667             else {
668             say "I'll be darned, $name mentioned $target at $source!";
669             }
670             }
671             else {
672             say "This webmention doesn't actually mention its target URL, "
673             . "so it is not verified.";
674             }
675              
676             # Manually buidling and sending a webmention:
677              
678             $wm = Web::Mention->new(
679             source => $url_of_the_thing_that_got_mentioned,
680             target => $url_of_the_thing_that_did_the_mentioning,
681             );
682              
683             my $success = $wm->send;
684             if ( $success ) {
685             say "Webmention sent successfully!";
686             }
687             else {
688             say "The webmention wasn't sent successfully.";
689             say "Here's the response we got back..."
690             say $wm->response->as_string;
691             }
692              
693             # Batch-sending a bunch of webmentions based on some published HTML
694              
695             my @wms = Web::Mention->new_from_html(
696             source => $url_of_a_web_page_i_just_published,
697             html => $relevant_html_content_of_that_web_page,
698             )
699              
700             for my $wm ( @wms ) {
701             my $success = $wm->send;
702             }
703              
704             =head1 DESCRIPTION
705              
706             This class implements the Webmention protocol, as defined by the W3C and
707             the IndieWeb community. (See L<this article by Chris
708             Aldrich|https://alistapart.com/article/webmentions-enabling-better-
709             communication-on-the-internet/> for an excellent high-level summary of
710             Webmention and its applications.)
711              
712             An object of this class represents a single webmention, with target and
713             source URLs. It can verify itself, determining whether or not the
714             document found at the source URL does indeed mention the target URL.
715              
716             It can also use IndieWeb algorithms to attempt identification of the
717             source document's author, and to provide a short summary of that
718             document's content, using Microformats2 metadata when available.
719              
720             =head1 METHODS
721              
722             =head2 Class Methods
723              
724             =head3 new
725              
726             $wm = Web::Mention->new(
727             source => $source_url,
728             target => $target_url,
729             );
730              
731             Basic constructor. The B<source> and B<target> URLs are both required
732             arguments. Either one can either be a L<URI> object, or a valid URL
733             string.
734              
735             Per the Webmention protocol, the B<source> URL represents the location
736             of the document that made the mention described here, and B<target>
737             describes the location of the document that got mentioned. The two
738             arguments cannot refer to the same URL (disregarding the C<#fragment>
739             part of either, if present).
740              
741             =head3 new_from_html
742              
743             @wms = Web::Mention->new_from_html(
744             source => $source_url,
745             html => $html,
746             );
747              
748             Convenience batch-construtor that returns a (possibly empty) I<list> of
749             Web::Mention objects based on the single source URL (or I<URI> object)
750             that you pass in, as well as a string containing HTML from which we can
751             extract zero or more target URLs. These extracted URLs include the
752             C<href> attribute value of every E<lt>aE<gt> tag in the provided HTML.
753              
754             Note that (as with all this class's constructors) this method won't
755             proceed to actually send the generated webmentions; that step remains
756             yours to take. (See L<"send">.)
757              
758             =head3 new_from_request
759              
760             $wm = Web::Mention->new_from_request( $request_object );
761              
762             Convenience constructor that looks into the given web-request object for
763             B<source> and B<target> parameters, and attempts to build a new
764             Web::Mention object out of them.
765              
766             The object must provide a C<param( $param_name )> method that returns
767             the value of the named HTTP parameter. So it could be a
768             L<Catalyst::Request> object or a L<Mojo::Message::Request> object, for
769             example.
770              
771             Throws an exception if the given argument doesn't meet this requirement,
772             or if it does but does not define both required HTTP parameters.
773              
774             =head3 FROM_JSON
775              
776             use JSON;
777              
778             $wm = Web::Mention->FROM_JSON(
779             JSON::decode_json( $serialized_webmention )
780             );
781              
782             Converts an unblessed hash reference resulting from an earlier
783             serialization (via L<JSON>) into a fully fledged Web::Mention object.
784             See L<"SERIALIZATION">.
785              
786             The all-caps spelling comes from a perhaps-misguided attempt to pair
787             well with the TO_JSON method that L<JSON> requires. As such, this method
788             may end up deprecated in favor of a less convoluted approach in future
789             releases of this module.
790              
791             =head3 content_truncation_marker
792              
793             Web::Mention->content_truncation_marker( $new_truncation_marker )
794              
795             The text that the content method will append to text that it has
796             truncated, if it did truncate it. (See L<"content">.)
797              
798             Defaults to C<...>.
799              
800             =head3 max_content_length
801              
802             Web::Mention->max_content_length( $new_max_length )
803              
804             Gets or sets the maximum length, in characters, of the content displayed
805             by that object method prior to truncation. (See L<"content">.)
806              
807             Defaults to 280.
808              
809             =head2 Object Methods
810              
811             =head3 author
812              
813             $author = $wm->author;
814              
815             A Web::Mention::Author object representing the author of this
816             webmention's source document, if we're able to determine it. If not,
817             this returns undef.
818              
819             =head3 content
820              
821             $content = $wm->content;
822              
823             Returns a string containing this object's best determination of this
824             webmention's I<display-ready> content, based on a number of factors.
825              
826             If the source document uses Microformats2 metadata and contains an
827             C<h-entry> MF2 item, then returned content may come from a variety of
828             its constituent properties, according to L<the IndieWeb comment-display
829             algorithm|https://indieweb.org/comments#How_to_display>.
830              
831             If not, then it returns the content of the source document's
832             E<lt>titleE<gt> element, with any further HTML stripped away.
833              
834             In any case, the string will get truncated if it's too long. See
835             L<"max_content_length"> and L<"content_truncation_marker">.
836              
837             =head3 endpoint
838              
839             my $uri = $wm->endpoint;
840              
841             Attempts to determine the webmention endpoint URL of this webmention's
842             target. On success, returns a L<URI> object. On failure, returns undef.
843              
844             (If the endpoint is set to localhost or a loopback IP, will return undef
845             and also emit a warning, because that's terribly rude behavior on the
846             target's part.)
847              
848             =head3 is_tested
849              
850             $bool = $wm->is_tested;
851              
852             Returns 1 if this object's L<"verify"> method has been called at least
853             once, regardless of the results of that call. Returns 0 otherwise.
854              
855             =head3 is_verified
856              
857             $bool = $wm->is_verified;
858              
859             Returns 1 if the webmention's source document actually does seem to
860             mention the target URL. Otherwise returns 0.
861              
862             The first time this is called on a given webmention object, it will try
863             to fetch the source document at its designated URL by way of the
864             L<"verify"> method.
865              
866             =head3 original_source
867              
868             $original_url = $wm->original_source;
869              
870             If the document fetched from the source URL seems to point at yet
871             another URL as its original source, then this returns that URL. If not,
872             this has the same return value as L<"source">.
873              
874             (It makes this determination based on the possible presence a C<u-url>
875             property in an C<h-entry> found within the source document.)
876              
877             =head3 response
878              
879             my $response = $wm->response;
880              
881             Returns the L<HTTP::Response> object representing the response received
882             by this webmention instance during its most recent attempt to send
883             itself.
884              
885             Returns undef if this webmention instance hasn't tried to send itself.
886              
887             =head3 rsvp_type
888              
889             my $rsvp = $wm->rsvp_type;
890              
891             If this webmention is of type C<rsvp> (see L<"type">, below), then this
892             method returns the type of RSVP represented. It will be one of:
893              
894             =over
895              
896             =item *
897              
898             yes
899              
900             =item *
901              
902             no
903              
904             =item *
905              
906             maybe
907              
908             =item *
909              
910             interested
911              
912             =back
913              
914             Otherwise, returns undef.
915              
916             =head3 send
917              
918             my $bool = $wm->send;
919              
920             Attempts to send an HTTP-request representation of this webmention to
921             its target's designated webmention endpoint. This involves querying the
922             target URL to discover said endpoint's URL (via the C<endpoint> object
923             method), and then sending the actual webmention request via HTTP to that
924             endpoint.
925              
926             If that whole process goes through successfully and the endpoint returns
927             a success response (meaning that it has acknowledged the webmention, and
928             most likely queued it for later processing), then this method returns
929             true. Otherwise, it returns false.
930              
931             B<To determine why a webmention did not send itself successfully>, consult
932             the value of C<response>. If it is defined, then you can call
933             L<HTTP::Response> methods (such as C<code> or C<message>) to learn more
934             about the problem. Otherwise, if C<response> is not defined, then the
935             target URL did not advertise a Webmention endpoint.
936              
937             =head3 source
938              
939             $source_url = $wm->source;
940              
941             Returns the webmention's source URL, as a L<URI> object.
942              
943             =head3 source_html
944              
945             $html = $wm->source_html;
946              
947             The HTML of the document fetched from the source URL. If nothing got
948             fetched successfully, returns undef.
949              
950             =head3 source_mf2_document
951              
952             $mf2_doc = $wm->source_mf2_document;
953              
954             The L<Web::Microformats2::Document> object that resulted from parsing
955             the source document for Microformats2 metadata. If no such result,
956             returns undef.
957              
958             =head3 target
959              
960             $target_url = $wm->target;
961              
962             Returns the webmention's target URL, as a L<URI> object.
963              
964             =head3 time_published
965              
966             $published_dt = $wm->time_published;
967              
968             If the document fetched from the source URL explicitly declares a
969             publication time via microformats, then this will return an appropriate
970             L<DateTime> object.
971              
972             If not (or if the declared time seems to be invalid), then this will
973             instead have the same return value as L<"time_received">.
974              
975             (It makes this determination based on the possible presence a C<dt-published>
976             property in an C<h-entry> found within the source document.)
977              
978             =head3 time_received
979              
980             $received_dt = $wm->time_received;
981              
982             A L<DateTime> object corresponding to this object's creation time.
983              
984             =head3 time_verified
985              
986             $verified_dt = $wm->time_verified;
987              
988             If this webmention has been verified, then this will return a
989             L<DateTime> object corresponding to the time of verification.
990             (Otherwise, returns undef.)
991              
992             =head3 title
993              
994             my $title = $wm->title;
995              
996             Returns a string containing this object's best determination of the
997             I<display-ready> title of this webmention's source document,
998             considered separately from its content. (You can get its more complete
999             content via the L<"content"> method.
1000              
1001             If the source document uses Microformats2 metadata and contains an
1002             C<h-entry> MF2 item, I<and> that item has a C<name> property, then this
1003             method will return the text content of that name property.
1004              
1005             If not, then it will return the content of the source document's
1006             E<lt>titleE<gt> element, with any further HTML stripped away.
1007              
1008             In any case, the string will get truncated if it's too long. See
1009             L<"max_content_length"> and L<"content_truncation_marker">.
1010              
1011             Note that in some circumstances, the title and content methods might
1012             return identical values. (If, for example, the source document defines
1013             an entry with an explicit name property and no summary or content
1014             properties.)
1015              
1016             =head3 type
1017              
1018             $type = $wm->type;
1019              
1020             The type of webmention this is. One of:
1021              
1022             =over
1023              
1024             =item *
1025              
1026             mention I<(default)>
1027              
1028             =item *
1029              
1030             reply
1031              
1032             =item *
1033              
1034             like
1035              
1036             =item *
1037              
1038             repost
1039              
1040             =item *
1041              
1042             quotation
1043              
1044             =item *
1045              
1046             rsvp
1047              
1048             =back
1049              
1050             This list is based on the W3C Post Type Discovery document
1051             (L<https://www.w3.org/TR/post-type-discovery/#response-algorithm>), and
1052             adds a "quotation" type.
1053              
1054             =head3 verify
1055              
1056             my $is_verified = $wm->verify
1057              
1058             This B<verifies> the webmention, confirming that the content located at
1059             the source URL contains the target URL. Returns 1 if so, and 0
1060             otherwise. Will also return 0 if it cannot fetch the content at all,
1061             after one try.
1062              
1063             Sets C<is_tested> to 1 as a side-effect.
1064              
1065             See also L<"is_verified">.
1066              
1067             =head1 SERIALIZATION
1068              
1069             To serialize a Web::Mention object into JSON, enable L<the JSON module's
1070             "convert_blessed" feature|JSON/"convert_blessed">, and then use one of
1071             that module's JSON-encoding functions on this object. This will result
1072             in a JSON string containing all the pertinent information about the
1073             webmention, including its verification status, any content and metadata
1074             fetched from the target, and so on.
1075              
1076             To unserialize a Web::Mention object serialized in this way, first
1077             decode it into an unblessed hash reference via L<JSON>, and then pass
1078             that as the single argument to L<the FROM_JSON class
1079             method|"FROM_JSON">.
1080              
1081             =head1 NOTES AND BUGS
1082              
1083             This software is B<beta>; its interface continues to develop and remains
1084             subject to change, but not without some effort at supporting its current
1085             API.
1086              
1087             This library does not, at this time, support L<the proposed "Vouch"
1088             anti-spam extension for Webmention|https://indieweb.org/Vouch>.
1089              
1090             =head1 SUPPORT
1091              
1092             To file issues or submit pull requests, please see L<this module's
1093             repository on GitHub|https://github.com/jmacdotorg/webmention-perl>.
1094              
1095             The author also welcomes any direct questions about this module via
1096             email.
1097              
1098             =head1 AUTHOR
1099              
1100             Jason McIntosh (jmac@jmac.org)
1101              
1102             =head1 CONTRIBUTORS
1103              
1104             =over
1105              
1106             =item *
1107              
1108             Mohammad S Anwar (mohammad.anwar@yahoo.com)
1109              
1110             =item *
1111              
1112             Tomaž Šolc (tomaz.solc@tablix.org)
1113              
1114             =back
1115              
1116             =head1 COPYRIGHT AND LICENSE
1117              
1118             This software is Copyright (c) 2018-2020 by Jason McIntosh.
1119              
1120             This is free software, licensed under:
1121              
1122             The MIT (X11) License
1123              
1124             =head1 A PERSONAL REQUEST
1125              
1126             My ability to share and maintain free, open-source software like this
1127             depends upon my living in a society that allows me the free time and
1128             personal liberty to create work benefiting people other than just myself
1129             or my immediate family. I recognize that I got a head start on this due
1130             to an accident of birth, and I strive to convert some of my unclaimed
1131             time and attention into work that, I hope, gives back to society in some
1132             small way.
1133              
1134             Worryingly, I find myself today living in a country experiencing a
1135             profound and unwelcome political upheaval, with its already flawed
1136             democracy under grave threat from powerful authoritarian elements. These
1137             powers wish to undermine this society, remolding it according to their
1138             deeply cynical and strictly zero-sum philosophies, where nobody can gain
1139             without someone else losing.
1140              
1141             Free and open-source software has no place in such a world. As such,
1142             these autocrats' further ascension would have a deleterious effect on my
1143             ability to continue working for the public good.
1144              
1145             Therefore, if you would like to financially support my work, I would ask
1146             you to consider a donation to one of the following causes. It would mean
1147             a lot to me if you did. (You can tell me about it if you'd like to, but
1148             you don't have to.)
1149              
1150             =over
1151              
1152             =item *
1153              
1154             L<The American Civil Liberties Union|https://aclu.org>
1155              
1156             =item *
1157              
1158             L<The Democratic National Committee|https://democrats.org>
1159              
1160             =item *
1161              
1162             L<Earthjustice|https://earthjustice.org>
1163              
1164             =back
1165