diff --git a/src/Endpoint.php b/src/Endpoint.php index 4e22a4b..411981f 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -54,42 +54,72 @@ class Endpoint { if ($response->getStatusCode() < 400) { - [$type, $content] = $this->parseContent($response->getContent(), $target); - $author = $this->parseAuthor($response->getContent()); + $document = new Crawler($response->getContent()); - $webmention = new Webmention(null, $target, $source, $type, $content, $author); + if (!$this->hasMention($target, $document)) + { + throw new TargetNotMentionedException(); + } + + $container = $this->getContainer($target, $document); + $type = $this->parseMentionType($target, $container); + $content = $this->parseContent($target, $container, $type); + $author = $this->parseAuthor($container); + + $webmention = new Webmention(null, $target, $source, $type, null, $author); $this->gateway->save($webmention); } else { throw new SourceNotFoundException(); } } - - private function parseContent(string $content, string $target) : array + private function hasMention(string $target, Crawler $document) : bool { - $body = new Crawler($content); - $anchors = $body->filter('a[href="'. $target . '"]'); - - if (!$anchors->count()) { - throw new TargetNotMentionedException(); - } - $type = $this->classToMentionType($anchors->attr('class')); - return [$type, null]; + return $document->filter('a[href="' . $target . '"]')->count() > 0; } - private function classToMentionType(string $class = "") : MentionType + private function getContainer(string $target, Crawler $document) : Crawler { + return $document->filter('a[href="' . $target . '"]')->closest('.h-entry') ?? $document; + } + + private function parseMentionType(string $target, Crawler $document) : MentionType + { + $class = $document->filter('a[href="'. $target . '"]')->attr('class'); + if (str_contains($class, "u-like-of")) { return MentionType::Like; } else if (str_contains($class, "u-in-reply-to")) { return MentionType::Reply; + } else if (str_contains($class, "u-repost-of")) { + return MentionType::Repost; } return MentionType::Mention; } - private function parseAuthor(string $author) : ?string + private function parseContent(string $target, Crawler $document, MentionType $type) : ?string { + return match ($type) { + MentionType::Like => "Liked this post", + MentionType::Reply => $document->innerText(), + MentionType::Repost => "Reposted this post", + MentionType::Mention => $document->closest('a[href="' . $target . '"]')->innerText() + }; + } + + private function parseAuthor(Crawler $document) : ?string + { + $card = $document->filter('.p-author.h-card')->eq(0); + + if ($card) + { + $name = $card->filter('.p-name')?->text(); + $url = $card->filter('.u-url')?->text(); + $photo = $card->filter('.u-photo')?->attr('src'); + + return implode(", ", [$name, $url, $photo]); + } return null; } } \ No newline at end of file diff --git a/src/Models/MentionType.php b/src/Models/MentionType.php index 9c94792..869d253 100644 --- a/src/Models/MentionType.php +++ b/src/Models/MentionType.php @@ -4,26 +4,28 @@ namespace Lewisdale\Webmentions\Models; enum MentionType { case Like; - case Comment; case Reply; case Mention; + case Repost; public function toString() : string { return match ($this) { MentionType::Like => "like", MentionType::Reply => "reply", - MentionType::Mention => "mention" + MentionType::Mention => "mention", + MentionType::Repost => "repost", }; } public static function from(string $string) : MentionType { - switch($string) { - case "like": return MentionType::Like; - case "reply": return MentionType::Reply; - default: return MentionType::Mention; - } + return match($string) { + "like" => MentionType::Like, + "reply" => MentionType::Reply, + "repost" => MentionType::Repost, + default => MentionType::Mention + }; } } ?> \ No newline at end of file diff --git a/tests/EndpointTest.php b/tests/EndpointTest.php index e1bdd0f..b808c63 100644 --- a/tests/EndpointTest.php +++ b/tests/EndpointTest.php @@ -14,6 +14,10 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; class EndpointTest extends TestCase { + private function objectContains(string $key, mixed $value) { + return $this->callback(fn(object $obj) => $obj->$key === $value); + } + #[TestWith(["https://my.url.com", true])] #[TestWith(["my.url.com", false])] public function testValidatesUrls(string $url, bool $expected) @@ -139,16 +143,7 @@ class EndpointTest extends TestCase { $mockGateway->expects($this->once()) ->method('save') - ->with($this->equalTo( - new Webmention( - null, - $target, - $source, - MentionType::Like, - null, - null - )) - ); + ->with($this->objectContains('type', MentionType::Like)); $endpoint = new Endpoint($mockClient, $mockGateway); @@ -189,16 +184,7 @@ class EndpointTest extends TestCase { $mockGateway->expects($this->once()) ->method('save') - ->with($this->equalTo( - new Webmention( - null, - $target, - $source, - MentionType::Mention, - null, - null - )) - ); + ->with($this->objectContains('type', MentionType::Mention)); $endpoint = new Endpoint($mockClient, $mockGateway); $endpoint->receiveWebmention($source, $target); @@ -238,19 +224,50 @@ class EndpointTest extends TestCase { $mockGateway->expects($this->once()) ->method('save') - ->with($this->equalTo( - new Webmention( - null, - $target, - $source, - MentionType::Reply, - null, - null - )) - ); + ->with($this->objectContains('type', MentionType::Reply)); $endpoint = new Endpoint($mockClient, $mockGateway); $endpoint->receiveWebmention($source, $target); } + + public function testItShouldParseAWebmentionAsARepost() + { + $source = "https://my-valid-source-url.com"; + $target = "https://lewisdale.dev/post/a-post-page"; + + $content = << + + + +

Some content

+

Here's some body content. It contains a url.

+

I'm writing about this post.

+ + + XML; + + $mockClient = $this->createMock(HttpClientInterface::class); + $mockResponse = $this->createMock(ResponseInterface::class); + $mockGateway = $this->createMock(WebmentionGatewayInterface::class); + + $mockClient->expects($this->once()) + ->method('request') + ->with($this->identicalTo('GET'), $this->identicalTo($source)) + ->will($this->returnValue($mockResponse)); + + $mockResponse->method('getStatusCode') + ->will($this->returnValue(200)); + + $mockResponse->method('getContent') + ->willReturn($content); + + $mockGateway->expects($this->once()) + ->method('save') + ->with($this->objectContains('type', MentionType::Repost)); + + $endpoint = new Endpoint($mockClient, $mockGateway); + $endpoint->receiveWebmention($source, $target); + } } ?> \ No newline at end of file diff --git a/tests/Gateways/SqliteGatewayTest.php b/tests/Gateways/SqliteGatewayTest.php index 811d760..9d0a33b 100644 --- a/tests/Gateways/SqliteGatewayTest.php +++ b/tests/Gateways/SqliteGatewayTest.php @@ -78,7 +78,7 @@ class SqliteGatewayTest extends TestCase null, "https://lewisdale.dev/post/a-new-post", "https://a-source.url", - MentionType::Comment, + MentionType::Reply, "No content", "Some Author Name" )); @@ -124,7 +124,7 @@ class SqliteGatewayTest extends TestCase null, "https://lewisdale.dev/post/a-new-post", "https://a-source.url", - MentionType::Comment, + MentionType::Reply, "Some content", "Some Author Name" ));