diff --git a/app/Console/Commands/PushGatewayRefresh.php b/app/Console/Commands/PushGatewayRefresh.php index 3a15154e6..79672695d 100644 --- a/app/Console/Commands/PushGatewayRefresh.php +++ b/app/Console/Commands/PushGatewayRefresh.php @@ -2,8 +2,9 @@ namespace App\Console\Commands; -use Illuminate\Console\Command; use App\Services\NotificationAppGatewayService; +use Illuminate\Console\Command; + use function Laravel\Prompts\select; class PushGatewayRefresh extends Command @@ -32,8 +33,9 @@ class PushGatewayRefresh extends Command $currentState = NotificationAppGatewayService::enabled(); - if($currentState) { + if ($currentState) { $this->info('Push Notification support is active!'); + return; } else { $this->error('Push notification support is NOT active'); @@ -44,22 +46,26 @@ class PushGatewayRefresh extends Command required: true ); - if($action === 'Yes') { + if ($action === 'Yes') { $recheck = NotificationAppGatewayService::forceSupportRecheck(); - if($recheck) { + if ($recheck) { $this->info('Success! Push Notifications are now active!'); + return; } else { $this->error('Error, please ensure you have a valid API key.'); $this->line(' '); $this->line('For more info, visit https://docs.pixelfed.org/running-pixelfed/push-notifications.html'); $this->line(' '); + return; } + return; } else { exit; } + return; } } diff --git a/app/Services/NotificationAppGatewayService.php b/app/Services/NotificationAppGatewayService.php index 661f1a17a..334c08547 100644 --- a/app/Services/NotificationAppGatewayService.php +++ b/app/Services/NotificationAppGatewayService.php @@ -2,10 +2,122 @@ namespace App\Services; +use Cache; +use Exception; +use Illuminate\Http\Client\RequestException; +use Illuminate\Support\Facades\Http; + class NotificationAppGatewayService { + const GATEWAY_SUPPORT_CHECK = 'px:nags:gateway-support-check'; + public static function config() { return config('instance.notifications.nag'); } + + public static function enabled() + { + if ((bool) config('instance.notifications.nag.enabled') === false) { + return false; + } + + $apiKey = config('instance.notifications.nag.api_key'); + if (! $apiKey || empty($apiKey) || strlen($apiKey) !== 45) { + return false; + } + + return Cache::remember(self::GATEWAY_SUPPORT_CHECK, 43200, function () { + return self::checkServerSupport(); + }); + } + + public static function checkServerSupport() + { + $endpoint = 'https://'.config('instance.notifications.nag.endpoint').'/api/v1/instance-check?domain='.config('pixelfed.domain.app'); + try { + $res = Http::withHeaders(['X-PIXELFED-API' => 1]) + ->retry(3, 500) + ->throw() + ->get($endpoint); + + $data = $res->json(); + } catch (RequestException $e) { + return false; + } catch (Exception $e) { + return false; + } + + if ($res->successful() && isset($data['active']) && $data['active'] === true) { + return true; + } + + return false; + } + + public static function forceSupportRecheck() + { + Cache::forget(self::GATEWAY_SUPPORT_CHECK); + + return self::enabled(); + } + + public static function isValidExpoPushToken($token) + { + if (! $token || empty($token)) { + return false; + } + $starts = str_starts_with($token, 'Expo'); + if (! $starts) { + return false; + } + if (! str_contains($token, 'PushToken[') || ! str_contains($token, ']')) { + return false; + } + if (substr($token, -1) !== ']') { + return false; + } + + return true; + } + + public static function send($userToken, $type, $actor = '') + { + if (! self::enabled()) { + return false; + } + + if (! $userToken || empty($userToken) || ! self::isValidExpoPushToken($userToken)) { + return false; + } + + $types = ['new_follower', 'like', 'comment', 'share']; + + if (! $type || empty($type) || ! in_array($type, $types)) { + return false; + } + + $apiKey = config('instance.notifications.nag.api_key'); + + if (! $apiKey || empty($apiKey)) { + return false; + } + $url = 'https://'.config('instance.notifications.nag.endpoint').'/api/v1/relay/deliver'; + + try { + $response = Http::withToken($apiKey) + ->withHeaders(['X-PIXELFED-API' => 1]) + ->post($url, [ + 'token' => $userToken, + 'type' => $type, + 'actor' => $actor, + ]); + + $response->throw(); + } catch (RequestException $e) { + return; + } catch (Exception $e) { + return; + } + } }