• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RikudouSage / lemmyverse.link / 14420750975

12 Apr 2025 03:01PM UTC coverage: 6.667% (-0.8%) from 7.5%
14420750975

push

github

RikudouSage
Add support for resolving any post following Lemmy convention

0 of 26 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

12 of 180 relevant lines covered (6.67%)

0.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Service/LemmyObjectResolver.php
1
<?php
2

3
namespace App\Service;
4

5
use App\Dto\ActivityPubItem;
6
use JsonException;
7
use LogicException;
8
use Psr\Cache\CacheItemPoolInterface;
9
use Rikudou\LemmyApi\Exception\LemmyApiException;
10
use Rikudou\LemmyApi\Response\Model\Comment;
11
use Rikudou\LemmyApi\Response\Model\Post;
12
use RuntimeException;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Contracts\Cache\ItemInterface;
16
use Symfony\Contracts\HttpClient\HttpClientInterface;
17

18
final readonly class LemmyObjectResolver
19
{
20
    public function __construct(
21
        private LemmyApiFactory $apiFactory,
22
        private CacheItemPoolInterface $cache,
23
        private HttpClientInterface $httpClient,
24
        private ActivityPubResolver $activityPubResolver,
25
    ) {
26
    }
×
27

28
    public function getPostId(int $originalPostId, string $originalInstance, string $targetInstance): ?int
29
    {
30
        if ($originalInstance === $targetInstance) {
×
31
            return $originalPostId;
×
32
        }
33

34
        $originalPost = $this->getPostById($originalInstance, $originalPostId);
×
NEW
35
        $activityPubId = $originalPost->id;
×
36

37
        try {
38
            $targetInstance = $this->getRealTargetInstance($targetInstance);
×
39
            $targetPost = $this->getPostByActivityPubId($targetInstance, $activityPubId);
×
40
        } catch (LogicException) {
×
41
            return null;
×
42
        }
43

44
        return $targetPost?->id;
×
45
    }
46

47
    public function getCommentId(int $originalCommentId, string $originalInstance, string $targetInstance): ?int
48
    {
49
        if ($originalInstance === $targetInstance) {
×
50
            return $originalCommentId;
×
51
        }
52

53
        $originalComment = $this->getCommentById($originalInstance, $originalCommentId);
×
54
        $activityPubId = $originalComment->apId;
×
55

56
        try {
57
            $targetInstance = $this->getRealTargetInstance($targetInstance);
×
58
            $targetComment = $this->getCommentByActivityPubId($targetInstance, $activityPubId);
×
59
        } catch (LogicException) {
×
60
            return null;
×
61
        }
62

63
        return $targetComment?->id;
×
64
    }
65

66
    public function getPostById(string $instance, int $postId): ActivityPubItem
67
    {
NEW
68
        $cacheItem = $this->cache->getItem("post_ap_{$instance}_{$postId}");
×
69
        if ($cacheItem->isHit()) {
×
70
            return $cacheItem->get(); // @phpstan-ignore-line
×
71
        }
72

NEW
73
        $post = $this->activityPubResolver->getItem("https://{$instance}/post/{$postId}");
×
NEW
74
        if (!$post) {
×
NEW
75
            throw new RuntimeException('Post not found');
×
76
        }
NEW
77
        $cacheItem->set($post);
×
UNCOV
78
        $this->cache->save($cacheItem);
×
79

NEW
80
        return $post;
×
81
    }
82

83
    public function getCommentById(string $instance, int $commentId): Comment
84
    {
85
        $cacheItem = $this->cache->getItem("comment_{$instance}_{$commentId}");
×
86
        if ($cacheItem->isHit()) {
×
87
            return $cacheItem->get(); // @phpstan-ignore-line
×
88
        }
89

90
        $api = $this->apiFactory->getForInstance($instance);
×
91
        $comment = $api->comment()->get($commentId);
×
92
        $cacheItem->set($comment->comment);
×
93
        $this->cache->save($cacheItem);
×
94

95
        return $comment->comment;
×
96
    }
97

98
    private function getPostByActivityPubId(string $instance, string $postId): ?Post
99
    {
100
        $cacheKeyPostId = str_replace(str_split(ItemInterface::RESERVED_CHARACTERS), '_', $postId);
×
101
        $cacheItem = $this->cache->getItem("post_{$instance}_{$cacheKeyPostId}");
×
102
        if ($cacheItem->isHit()) {
×
103
            return $cacheItem->get(); // @phpstan-ignore-line
×
104
        }
105

106
        $api = $this->apiFactory->getForInstance($instance);
×
107

108
        try {
109
            $post = $api->miscellaneous()->resolveObject(query: $postId)->post;
×
110
        } catch (LemmyApiException) {
×
111
            return null;
×
112
        }
113
        if ($post === null) {
×
114
            return null;
×
115
        }
116
        $cacheItem->set($post->post);
×
117
        $this->cache->save($cacheItem);
×
118

119
        return $post->post;
×
120
    }
121

122
    private function getCommentByActivityPubId(string $instance, string $commentId): ?Comment
123
    {
124
        $cacheKeyCommentId = str_replace(str_split(ItemInterface::RESERVED_CHARACTERS), '_', $commentId);
×
125
        $cacheItem = $this->cache->getItem("comment_{$instance}_{$cacheKeyCommentId}");
×
126
        if ($cacheItem->isHit()) {
×
127
            return $cacheItem->get(); // @phpstan-ignore-line
×
128
        }
129

130
        $api = $this->apiFactory->getForInstance($instance);
×
131

132
        try {
133
            $comment = $api->miscellaneous()->resolveObject(query: $commentId)->comment;
×
134
        } catch (LemmyApiException) {
×
135
            return null;
×
136
        }
137
        if ($comment === null) {
×
138
            return null;
×
139
        }
140
        $cacheItem->set($comment->comment);
×
141
        $this->cache->save($cacheItem);
×
142

143
        return $comment->comment;
×
144
    }
145

146
    private function getRealTargetInstance(string $targetInstance, ?string $instanceToCheck = null): string
147
    {
148
        $instanceToCheck ??= $targetInstance;
×
149

150
        $result = fn () => $this->getRealTargetInstance($targetInstance, $this->getParentDomain($instanceToCheck));
×
151

152
        $cacheItem = $this->cache->getItem("target_instance_{$targetInstance}");
×
153
        if ($cacheItem->isHit()) {
×
154
            return $cacheItem->get(); // @phpstan-ignore-line
×
155
        }
156

157
        $url = "https://{$instanceToCheck}/.well-known/nodeinfo";
×
158
        $response = $this->httpClient->request(Request::METHOD_GET, $url);
×
159
        if ($response->getStatusCode() !== Response::HTTP_OK) {
×
160
            return $result();
×
161
        }
162

163
        try {
164
            $json = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
×
165
            assert(is_array($json));
×
166
        } catch (JsonException) {
×
167
            return $result();
×
168
        }
169

170
        if (!isset($json['links'][0]['href'])) {
×
171
            return $result();
×
172
        }
173

174
        $response = $this->httpClient->request(Request::METHOD_GET, $json['links'][0]['href']);
×
175
        if ($response->getStatusCode() !== Response::HTTP_OK) {
×
176
            return $result();
×
177
        }
178

179
        try {
180
            $json = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
×
181
            assert(is_array($json));
×
182
        } catch (JsonException) {
×
183
            return $result();
×
184
        }
185

186
        if (($json['software']['name'] ?? null) !== 'lemmy') {
×
187
            return $result();
×
188
        }
189
        $cacheItem->set($instanceToCheck);
×
190
        $this->cache->save($cacheItem);
×
191

192
        return $instanceToCheck;
×
193
    }
194

195
    private function getParentDomain(string $domain): string
196
    {
197
        $parts = explode('.', $domain);
×
198
        if (count($parts) <= 2) {
×
199
            throw new LogicException('Cannot get a parent domain for a top level domain');
×
200
        }
201

202
        unset($parts[0]);
×
203

204
        return implode('.', $parts);
×
205
    }
206
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc