From 39c86816a467d962b2b02fde9bb21603ae273dc6 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 27 Mar 2026 11:24:34 +0100 Subject: [PATCH 1/2] wip --- src/Advocacy/AdvocacyPullCommand.php | 32 +++++++++++++++++++++++++ src/Advocacy/Discord/Discord.php | 17 +++++++++++++ src/Advocacy/Discord/DiscordConfig.php | 10 ++++++++ src/Advocacy/Discord/discord.config.php | 7 ++++++ src/Advocacy/Discord/reddit.config.php | 8 +++++++ src/Advocacy/Message.php | 11 +++++++++ src/Advocacy/Reddit/Reddit.php | 24 +++++++++++++++++++ src/Advocacy/Reddit/RedditConfig.php | 12 ++++++++++ 8 files changed, 121 insertions(+) create mode 100644 src/Advocacy/AdvocacyPullCommand.php create mode 100644 src/Advocacy/Discord/Discord.php create mode 100644 src/Advocacy/Discord/DiscordConfig.php create mode 100644 src/Advocacy/Discord/discord.config.php create mode 100644 src/Advocacy/Discord/reddit.config.php create mode 100644 src/Advocacy/Message.php create mode 100644 src/Advocacy/Reddit/Reddit.php create mode 100644 src/Advocacy/Reddit/RedditConfig.php diff --git a/src/Advocacy/AdvocacyPullCommand.php b/src/Advocacy/AdvocacyPullCommand.php new file mode 100644 index 0000000..5dace98 --- /dev/null +++ b/src/Advocacy/AdvocacyPullCommand.php @@ -0,0 +1,32 @@ +reddit->fetch(); + + foreach ($messages as $message) { + $this->discord->notify($message); + } + } +} diff --git a/src/Advocacy/Discord/Discord.php b/src/Advocacy/Discord/Discord.php new file mode 100644 index 0000000..4843f7b --- /dev/null +++ b/src/Advocacy/Discord/Discord.php @@ -0,0 +1,17 @@ +config->subreddits as $subreddit) { + // TODO: fetch posts and comments + + // TODO: loop over posts and comments + + // TODO: if their content matches the keywords, add them to $messages + } + + return $messages; + } +} \ No newline at end of file diff --git a/src/Advocacy/Reddit/RedditConfig.php b/src/Advocacy/Reddit/RedditConfig.php new file mode 100644 index 0000000..96789c0 --- /dev/null +++ b/src/Advocacy/Reddit/RedditConfig.php @@ -0,0 +1,12 @@ + Date: Fri, 27 Mar 2026 13:32:35 +0100 Subject: [PATCH 2/2] wip --- deploy.sh | 2 +- ...ullCommand.php => AdvocacySyncCommand.php} | 16 ++- src/Advocacy/Discord/Discord.php | 14 ++- src/Advocacy/Discord/DiscordConfig.php | 6 +- src/Advocacy/Discord/discord.config.php | 7 +- src/Advocacy/Discord/reddit.config.php | 8 -- src/Advocacy/Message.php | 23 ++++- src/Advocacy/Reddit/Reddit.php | 53 ++++++++-- src/Advocacy/Reddit/RedditApi.php | 97 +++++++++++++++++++ src/Advocacy/Reddit/RedditConfig.php | 19 +++- src/Advocacy/Reddit/RedditMessageFactory.php | 72 ++++++++++++++ src/Advocacy/Reddit/reddit.config.php | 18 ++++ src/Advocacy/advocacy-cache.config.php | 9 ++ 13 files changed, 312 insertions(+), 32 deletions(-) rename src/Advocacy/{AdvocacyPullCommand.php => AdvocacySyncCommand.php} (53%) delete mode 100644 src/Advocacy/Discord/reddit.config.php create mode 100644 src/Advocacy/Reddit/RedditApi.php create mode 100644 src/Advocacy/Reddit/RedditMessageFactory.php create mode 100644 src/Advocacy/Reddit/reddit.config.php create mode 100644 src/Advocacy/advocacy-cache.config.php diff --git a/deploy.sh b/deploy.sh index 4cdf09d..b060c44 100644 --- a/deploy.sh +++ b/deploy.sh @@ -18,7 +18,7 @@ php8.5 tempest static:clean --force php8.5 tempest docs:pull --no-interaction php8.5 tempest command-palette:index /home/forge/.bun/bin/bun run build -php8.5 tempest cache:clear --force +php8.5 tempest cache:clear --tag=default --force php8.5 tempest view:clear --force php8.5 tempest static:generate --verbose=true diff --git a/src/Advocacy/AdvocacyPullCommand.php b/src/Advocacy/AdvocacySyncCommand.php similarity index 53% rename from src/Advocacy/AdvocacyPullCommand.php rename to src/Advocacy/AdvocacySyncCommand.php index 5dace98..0340255 100644 --- a/src/Advocacy/AdvocacyPullCommand.php +++ b/src/Advocacy/AdvocacySyncCommand.php @@ -11,7 +11,7 @@ use Tempest\Console\Schedule; use Tempest\Console\Scheduler\Every; -final readonly class AdvocacyPullCommand +final readonly class AdvocacySyncCommand { use HasConsole; @@ -20,13 +20,21 @@ public function __construct( private Discord $discord, ) {} - #[ConsoleCommand('advocacy:pull'), Schedule(Every::HALF_HOUR)] - public function __invoke(): void + #[ConsoleCommand, Schedule(Every::HALF_HOUR)] + public function __invoke(bool $sync = true): void { $messages = $this->reddit->fetch(); +// $messages = [new Message('1', 'Test', ['tempest'], 'https://tempestphp.com')]; + foreach ($messages as $message) { - $this->discord->notify($message); + if ($sync) { + $this->discord->notify($message); + } + + $this->info('[' . implode(',', $message->matches) . '] ' . $message->uri); } + + $this->success('Done'); } } diff --git a/src/Advocacy/Discord/Discord.php b/src/Advocacy/Discord/Discord.php index 4843f7b..7f04edd 100644 --- a/src/Advocacy/Discord/Discord.php +++ b/src/Advocacy/Discord/Discord.php @@ -1,17 +1,27 @@ http->post( + uri: $this->config->webhookUrl, + headers: ['Content-Type' => 'application/json'], + body: json_encode([ + 'content' => $message->uri, + ]), + ); } -} \ No newline at end of file +} diff --git a/src/Advocacy/Discord/DiscordConfig.php b/src/Advocacy/Discord/DiscordConfig.php index dbd12d9..8b9bd4d 100644 --- a/src/Advocacy/Discord/DiscordConfig.php +++ b/src/Advocacy/Discord/DiscordConfig.php @@ -1,10 +1,12 @@ str($this->body) + ->substr(0, 1200) + ->excerpt( + from: 0, + to: 20, + asArray: true, + ) + ->map(fn (string $line) => "> " . $line) + ->implode(PHP_EOL) + ->toString(); + } + public function __construct( - public string $message, + public string $id, + public string $body, + public array $matches, public string $uri, ) {} -} \ No newline at end of file +} diff --git a/src/Advocacy/Reddit/Reddit.php b/src/Advocacy/Reddit/Reddit.php index fa5be31..ffb1b5e 100644 --- a/src/Advocacy/Reddit/Reddit.php +++ b/src/Advocacy/Reddit/Reddit.php @@ -1,24 +1,63 @@ config->subreddits as $subreddit) { - // TODO: fetch posts and comments + foreach ($this->config->filters as $subreddit => $keywords) { + $messages = [ + ...$messages, + ...$this->fetchForSubreddit($subreddit, $keywords), + ]; + } + + return $messages; + } + + /** @return Message[] */ + private function fetchForSubreddit(string $subreddit, array $keywords): array + { + $posts = $this->api->get("/r/{$subreddit}/new"); + $comments = $this->api->get("/r/{$subreddit}/comments"); + + return $this->makeMessages([...$posts, ...$comments], $subreddit, $keywords); + } + + /** @return Message[] */ + private function makeMessages(array $items, string $subreddit, array $keywords): array + { + $messages = []; + + foreach ($items as $item) { + $message = $this->messageFactory->fromRedditItem($item, $keywords); + + if ($message === null || $this->cache->has($message->id)) { + continue; + } - // TODO: loop over posts and comments + $this->cache->put($message->id, true); - // TODO: if their content matches the keywords, add them to $messages + $messages[] = $message; } return $messages; } -} \ No newline at end of file +} diff --git a/src/Advocacy/Reddit/RedditApi.php b/src/Advocacy/Reddit/RedditApi.php new file mode 100644 index 0000000..075cf53 --- /dev/null +++ b/src/Advocacy/Reddit/RedditApi.php @@ -0,0 +1,97 @@ +config->token = $this->resolveToken(); + } + + private function resolveToken(): string + { + return $this->cache->resolve('reddit.access-token', function () { + $response = $this->http->post( + uri: 'https://www.reddit.com/api/v1/access_token?grant_type=client_credentials', + headers: [ + 'Authorization' => 'Basic ' . base64_encode("{$this->config->clientId}:{$this->config->clientSecret}"), + 'User-Agent' => $this->config->userAgent, + ], + ); + + $payload = $this->decodeJson($response->body); + $token = $payload['access_token'] ?? null; + + if (! is_string($token) || $token === '') { + throw new RuntimeException('Failed to fetch Reddit access token.'); + } + + return $token; + }, Duration::minutes(50)); + } + + public function get(string $path): array + { + $cacheKey = str("reddit.{$path}")->slug()->toString(); + + return $this->cache->resolve($cacheKey, function () use ($path) { + $response = $this->http->get( + uri: "https://oauth.reddit.com{$path}?limit={$this->config->limit}", + headers: [ + 'Authorization' => "Bearer {$this->config->token}", + 'User-Agent' => $this->config->userAgent, + ], + ); + + $payload = $this->decodeJson($response->body); + $children = $payload['data']['children'] ?? null; + + if (! is_array($children)) { + return []; + } + + $items = []; + + foreach ($children as $child) { + $data = $child['data'] ?? null; + + if (! is_array($data)) { + continue; + } + + $items[] = $data; + } + + return $items; + }, Duration::days(24)); + } + + private function decodeJson(mixed $body): array + { + if (is_array($body)) { + return $body; + } + + if (! is_string($body) || $body === '') { + return []; + } + + $decoded = json_decode($body, associative: true); + + return is_array($decoded) ? $decoded : []; + } +} diff --git a/src/Advocacy/Reddit/RedditConfig.php b/src/Advocacy/Reddit/RedditConfig.php index 96789c0..edb96aa 100644 --- a/src/Advocacy/Reddit/RedditConfig.php +++ b/src/Advocacy/Reddit/RedditConfig.php @@ -1,12 +1,23 @@ > */ + public array $filters, + #[SensitiveParameter] public string $clientId, + #[SensitiveParameter] public string $clientSecret, + public string $userAgent = 'TempestAdvocacyBot/1.0', + public int $limit = 50, ) {} -} \ No newline at end of file +} diff --git a/src/Advocacy/Reddit/RedditMessageFactory.php b/src/Advocacy/Reddit/RedditMessageFactory.php new file mode 100644 index 0000000..d6fa562 --- /dev/null +++ b/src/Advocacy/Reddit/RedditMessageFactory.php @@ -0,0 +1,72 @@ +resolveId($item); + + if ($id === null) { + return null; + } + + $matchedKeyword = $this->findMatchedKeyword($item, $keywords); + + if ($matchedKeyword === null) { + return null; + } + + $body = trim((string) ($item['selftext'] ?? $item['body'] ?? '')); + + return new Message( + id: "advocacy.reddit.message.{$id}", + body: $body, + matches: [$matchedKeyword], + uri: $this->resolveUri($item), + ); + } + + private function findMatchedKeyword(array $item, array $keywords): ?string + { + $content = mb_strtolower(implode(' ', [ + (string) ($item['title'] ?? ''), + (string) ($item['selftext'] ?? ''), + (string) ($item['body'] ?? ''), + (string) ($item['link_title'] ?? ''), + ])); + + foreach ($keywords as $keyword) { + if (mb_stripos($content, mb_strtolower($keyword)) === false) { + continue; + } + + return $keyword; + } + + return null; + } + + private function resolveId(array $item): ?string + { + $id = $item['name'] ?? $item['id'] ?? null; + + return is_string($id) && $id !== '' ? $id : null; + } + + private function resolveUri(array $item): string + { + $permalink = (string) ($item['permalink'] ?? ''); + + if ($permalink !== '') { + return 'https://reddit.com' . $permalink; + } + + return (string) ($item['url'] ?? 'https://reddit.com'); + } +} diff --git a/src/Advocacy/Reddit/reddit.config.php b/src/Advocacy/Reddit/reddit.config.php new file mode 100644 index 0000000..c3902bc --- /dev/null +++ b/src/Advocacy/Reddit/reddit.config.php @@ -0,0 +1,18 @@ + ['tempest', 'framework'], + 'laravel' => ['tempest'], + 'symfony' => ['tempest'], + ], + clientId: (string) env('REDDIT_CLIENT_ID', ''), + clientSecret: (string) env('REDDIT_CLIENT_SECRET', ''), + userAgent: (string) env('REDDIT_USER_AGENT', 'TempestAdvocacyBot/1.0'), + limit: (int) env('REDDIT_LIMIT', 50), +); diff --git a/src/Advocacy/advocacy-cache.config.php b/src/Advocacy/advocacy-cache.config.php new file mode 100644 index 0000000..f04a27c --- /dev/null +++ b/src/Advocacy/advocacy-cache.config.php @@ -0,0 +1,9 @@ +