From 33621dd68033b923e59636518da4234ea4ccdd29 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 18 Sep 2024 02:29:38 -0600 Subject: [PATCH 01/20] Add Notify App Gateway support --- .../Controllers/Api/ApiV1Dot1Controller.php | 18 +++++++++++++----- app/Services/NotificationAppGatewayService.php | 11 +++++++++++ config/instance.php | 6 ++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 app/Services/NotificationAppGatewayService.php diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index efd04c60d..ac2c1f55a 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -26,6 +26,7 @@ use App\Services\FollowerService; use App\Services\MediaBlocklistService; use App\Services\MediaPathService; use App\Services\NetworkTimelineService; +use App\Services\NotificationAppGatewayService; use App\Services\ProfileStatusService; use App\Services\PublicTimelineService; use App\Services\StatusService; @@ -54,8 +55,8 @@ class ApiV1Dot1Controller extends Controller public function __construct() { - $this->fractal = new Fractal\Manager(); - $this->fractal->setSerializer(new ArraySerializer()); + $this->fractal = new Fractal\Manager; + $this->fractal->setSerializer(new ArraySerializer); } public function json($res, $code = 200, $headers = []) @@ -317,7 +318,7 @@ class ApiV1Dot1Controller extends Controller if (config('pixelfed.bouncer.cloud_ips.ban_signups')) { abort_if(BouncerService::checkIp($request->ip()), 404); } - $agent = new Agent(); + $agent = new Agent; $currentIp = $request->ip(); $activity = AccountLog::whereUserId($user->id) @@ -575,7 +576,7 @@ class ApiV1Dot1Controller extends Controller $rtoken = Str::random(64); - $verify = new EmailVerification(); + $verify = new EmailVerification; $verify->user_id = $user->id; $verify->email = $user->email; $verify->user_token = $user->app_register_token; @@ -1203,7 +1204,7 @@ class ApiV1Dot1Controller extends Controller abort(500, 'An error occured.'); } - $media = new Media(); + $media = new Media; $media->status_id = $status->id; $media->profile_id = $profile->id; $media->user_id = $user->id; @@ -1252,4 +1253,11 @@ class ApiV1Dot1Controller extends Controller return $this->json($res); } + + public function nagState(Request $request) + { + abort_unless((bool) config_cache('pixelfed.oauth_enabled'), 404); + + return NotificationAppGatewayService::config(); + } } diff --git a/app/Services/NotificationAppGatewayService.php b/app/Services/NotificationAppGatewayService.php new file mode 100644 index 000000000..661f1a17a --- /dev/null +++ b/app/Services/NotificationAppGatewayService.php @@ -0,0 +1,11 @@ + env('INSTANCE_NOTIFY_AUTO_GC', false), 'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365), ], + + 'nag' => [ + 'enabled' => (bool) env('INSTANCE_NOTIFY_APP_GATEWAY', true), + 'endpoint' => 'push.pixelfed.net', + ], ], 'curated_registration' => [ @@ -171,6 +176,7 @@ return [ 'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false), 'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false), 'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10), + 'cc_addresses' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_CC'), ], 'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false), ], From 669f8b280b54b41d8b44fbe0fc814e1479692b33 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 18 Sep 2024 02:32:07 -0600 Subject: [PATCH 02/20] Update api routes --- routes/api.php | 67 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/routes/api.php b/routes/api.php index 524e91d9b..e996e5485 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,10 +1,8 @@ middleware($middleware)->group(function() { +Route::prefix('api/v0/groups')->middleware($middleware)->group(function () { Route::get('config', 'Groups\GroupsApiController@getConfig'); Route::post('permission/create', 'Groups\CreateGroupsController@checkCreatePermission'); Route::post('create', 'Groups\CreateGroupsController@storeGroup'); @@ -87,9 +85,9 @@ Route::prefix('api/v0/groups')->middleware($middleware)->group(function() { Route::get('{id}', 'GroupController@getGroup'); }); -Route::group(['prefix' => 'api'], function() use($middleware) { +Route::group(['prefix' => 'api'], function () use ($middleware) { - Route::group(['prefix' => 'v1'], function() use($middleware) { + Route::group(['prefix' => 'v1'], function () use ($middleware) { Route::post('apps', 'Api\ApiV1Controller@apps'); Route::get('apps/verify_credentials', 'Api\ApiV1Controller@getApp')->middleware($middleware); Route::get('instance', 'Api\ApiV1Controller@instance'); @@ -170,7 +168,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('statuses/{id}/history', 'StatusEditController@history')->middleware($middleware); Route::put('statuses/{id}', 'StatusEditController@store')->middleware($middleware); - Route::group(['prefix' => 'admin'], function() use($middleware) { + Route::group(['prefix' => 'admin'], function () use ($middleware) { Route::get('domain_blocks', 'Api\V1\Admin\DomainBlocksController@index')->middleware($middleware); Route::post('domain_blocks', 'Api\V1\Admin\DomainBlocksController@create')->middleware($middleware); Route::get('domain_blocks/{id}', 'Api\V1\Admin\DomainBlocksController@show')->middleware($middleware); @@ -179,17 +177,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { })->middleware($middleware); }); - Route::group(['prefix' => 'v2'], function() use($middleware) { + Route::group(['prefix' => 'v2'], function () use ($middleware) { Route::get('search', 'Api\ApiV2Controller@search')->middleware($middleware); Route::post('media', 'Api\ApiV2Controller@mediaUploadV2')->middleware($middleware); Route::get('streaming/config', 'Api\ApiV2Controller@getWebsocketConfig'); Route::get('instance', 'Api\ApiV2Controller@instance'); }); - Route::group(['prefix' => 'v1.1'], function() use($middleware) { + Route::group(['prefix' => 'v1.1'], function () use ($middleware) { Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($middleware); - Route::group(['prefix' => 'accounts'], function () use($middleware) { + Route::group(['prefix' => 'accounts'], function () use ($middleware) { Route::get('timelines/home', 'Api\ApiV1Controller@timelineHome')->middleware($middleware); Route::delete('avatar', 'Api\ApiV1Dot1Controller@deleteAvatar')->middleware($middleware); Route::get('{id}/posts', 'Api\ApiV1Dot1Controller@accountPosts')->middleware($middleware); @@ -202,7 +200,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('username/{username}', 'Api\ApiV1Dot1Controller@accountUsernameToId')->middleware($middleware); }); - Route::group(['prefix' => 'collections'], function () use($middleware) { + Route::group(['prefix' => 'collections'], function () use ($middleware) { Route::get('accounts/{id}', 'CollectionController@getUserCollections')->middleware($middleware); Route::get('items/{id}', 'CollectionController@getItems')->middleware($middleware); Route::get('view/{id}', 'CollectionController@getCollection')->middleware($middleware); @@ -213,7 +211,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('self', 'CollectionController@getSelfCollections')->middleware($middleware); }); - Route::group(['prefix' => 'direct'], function () use($middleware) { + Route::group(['prefix' => 'direct'], function () use ($middleware) { Route::get('thread', 'DirectMessageController@thread')->middleware($middleware); Route::post('thread/send', 'DirectMessageController@create')->middleware($middleware); Route::delete('thread/message', 'DirectMessageController@delete')->middleware($middleware); @@ -224,17 +222,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::post('lookup', 'DirectMessageController@composeLookup')->middleware($middleware); }); - Route::group(['prefix' => 'archive'], function () use($middleware) { + Route::group(['prefix' => 'archive'], function () use ($middleware) { Route::post('add/{id}', 'Api\ApiV1Dot1Controller@archive')->middleware($middleware); Route::post('remove/{id}', 'Api\ApiV1Dot1Controller@unarchive')->middleware($middleware); Route::get('list', 'Api\ApiV1Dot1Controller@archivedPosts')->middleware($middleware); }); - Route::group(['prefix' => 'places'], function () use($middleware) { + Route::group(['prefix' => 'places'], function () use ($middleware) { Route::get('posts/{id}/{slug}', 'Api\ApiV1Dot1Controller@placesById')->middleware($middleware); }); - Route::group(['prefix' => 'stories'], function () use($middleware) { + Route::group(['prefix' => 'stories'], function () use ($middleware) { Route::get('carousel', 'Stories\StoryApiV1Controller@carousel')->middleware($middleware); Route::post('add', 'Stories\StoryApiV1Controller@add')->middleware($middleware); Route::post('publish', 'Stories\StoryApiV1Controller@publish')->middleware($middleware); @@ -243,23 +241,23 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::post('comment', 'Stories\StoryApiV1Controller@comment')->middleware($middleware); }); - Route::group(['prefix' => 'compose'], function () use($middleware) { + Route::group(['prefix' => 'compose'], function () use ($middleware) { Route::get('search/location', 'ComposeController@searchLocation')->middleware($middleware); Route::get('settings', 'ComposeController@composeSettings')->middleware($middleware); }); - Route::group(['prefix' => 'discover'], function () use($middleware) { + Route::group(['prefix' => 'discover'], function () use ($middleware) { Route::get('accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular')->middleware($middleware); Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware); Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware); Route::get('posts/network/trending', 'DiscoverController@discoverNetworkTrending')->middleware($middleware); }); - Route::group(['prefix' => 'directory'], function () use($middleware) { + Route::group(['prefix' => 'directory'], function () { Route::get('listing', 'PixelfedDirectoryController@get'); }); - Route::group(['prefix' => 'auth'], function () use($middleware) { + Route::group(['prefix' => 'auth'], function () { Route::get('iarpfc', 'Api\ApiV1Dot1Controller@inAppRegistrationPreFlightCheck'); Route::post('iar', 'Api\ApiV1Dot1Controller@inAppRegistration'); Route::post('iarc', 'Api\ApiV1Dot1Controller@inAppRegistrationConfirm'); @@ -270,16 +268,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::post('invite/admin/ec', 'AdminInviteController@apiEmailCheck')->middleware('throttle:10,1440'); }); - Route::group(['prefix' => 'expo'], function() use($middleware) { + Route::group(['prefix' => 'expo'], function () use ($middleware) { Route::get('push-notifications', 'Api\ApiV1Dot1Controller@getExpoPushNotifications')->middleware($middleware); Route::post('push-notifications/update', 'Api\ApiV1Dot1Controller@updateExpoPushNotifications')->middleware($middleware); Route::post('push-notifications/disable', 'Api\ApiV1Dot1Controller@disableExpoPushNotifications')->middleware($middleware); }); Route::post('status/create', 'Api\ApiV1Dot1Controller@statusCreate')->middleware($middleware); + Route::get('nag/state', 'Api\ApiV1Dot1Controller@nagState'); }); - Route::group(['prefix' => 'live'], function() use($middleware) { + Route::group(['prefix' => 'live'], function () { // Route::post('create_stream', 'LiveStreamController@createStream')->middleware($middleware); // Route::post('stream/edit', 'LiveStreamController@editStream')->middleware($middleware); // Route::get('active/list', 'LiveStreamController@getActiveStreams')->middleware($middleware); @@ -297,7 +296,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { // Route::post('broadcast/finish', 'LiveStreamController@clientBroadcastFinish')->middleware($middleware); }); - Route::group(['prefix' => 'admin'], function() use($middleware) { + Route::group(['prefix' => 'admin'], function () use ($middleware) { Route::post('moderate/post/{id}', 'Api\ApiV1Dot1Controller@moderatePost')->middleware($middleware); Route::get('supported', 'Api\AdminApiController@supported')->middleware($middleware); Route::get('stats', 'Api\AdminApiController@getStats')->middleware($middleware); @@ -318,15 +317,15 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('instance/stats', 'Api\AdminApiController@getAllStats')->middleware($middleware); }); - Route::group(['prefix' => 'landing/v1'], function() use($middleware) { + Route::group(['prefix' => 'landing/v1'], function () { Route::get('directory', 'LandingController@getDirectoryApi'); }); - Route::group(['prefix' => 'pixelfed'], function() use($middleware) { - Route::group(['prefix' => 'v1'], function() use($middleware) { + Route::group(['prefix' => 'pixelfed'], function () use ($middleware) { + Route::group(['prefix' => 'v1'], function () use ($middleware) { Route::post('report', 'Api\ApiV1Dot1Controller@report')->middleware($middleware); - Route::group(['prefix' => 'accounts'], function () use($middleware) { + Route::group(['prefix' => 'accounts'], function () use ($middleware) { Route::get('timelines/home', 'Api\ApiV1Controller@timelineHome')->middleware($middleware); Route::delete('avatar', 'Api\ApiV1Dot1Controller@deleteAvatar')->middleware($middleware); Route::get('{id}/posts', 'Api\ApiV1Dot1Controller@accountPosts')->middleware($middleware); @@ -337,13 +336,13 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('apps-and-applications', 'Api\ApiV1Dot1Controller@accountApps')->middleware($middleware); }); - Route::group(['prefix' => 'archive'], function () use($middleware) { + Route::group(['prefix' => 'archive'], function () use ($middleware) { Route::post('add/{id}', 'Api\ApiV1Dot1Controller@archive')->middleware($middleware); Route::post('remove/{id}', 'Api\ApiV1Dot1Controller@unarchive')->middleware($middleware); Route::get('list', 'Api\ApiV1Dot1Controller@archivedPosts')->middleware($middleware); }); - Route::group(['prefix' => 'collections'], function () use($middleware) { + Route::group(['prefix' => 'collections'], function () use ($middleware) { Route::get('accounts/{id}', 'CollectionController@getUserCollections')->middleware($middleware); Route::get('items/{id}', 'CollectionController@getItems')->middleware($middleware); Route::get('view/{id}', 'CollectionController@getCollection')->middleware($middleware); @@ -354,12 +353,12 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('self', 'CollectionController@getSelfCollections')->middleware($middleware); }); - Route::group(['prefix' => 'compose'], function () use($middleware) { + Route::group(['prefix' => 'compose'], function () use ($middleware) { Route::get('search/location', 'ComposeController@searchLocation')->middleware($middleware); Route::get('settings', 'ComposeController@composeSettings')->middleware($middleware); }); - Route::group(['prefix' => 'direct'], function () use($middleware) { + Route::group(['prefix' => 'direct'], function () use ($middleware) { Route::get('thread', 'DirectMessageController@thread')->middleware($middleware); Route::post('thread/send', 'DirectMessageController@create')->middleware($middleware); Route::delete('thread/message', 'DirectMessageController@delete')->middleware($middleware); @@ -370,17 +369,17 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::post('lookup', 'DirectMessageController@composeLookup')->middleware($middleware); }); - Route::group(['prefix' => 'discover'], function () use($middleware) { + Route::group(['prefix' => 'discover'], function () use ($middleware) { Route::get('accounts/popular', 'Api\ApiV1Controller@discoverAccountsPopular')->middleware($middleware); Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware); Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware); }); - Route::group(['prefix' => 'directory'], function () use($middleware) { + Route::group(['prefix' => 'directory'], function () { Route::get('listing', 'PixelfedDirectoryController@get'); }); - Route::group(['prefix' => 'places'], function () use($middleware) { + Route::group(['prefix' => 'places'], function () use ($middleware) { Route::get('posts/{id}/{slug}', 'Api\ApiV1Dot1Controller@placesById')->middleware($middleware); }); @@ -389,7 +388,7 @@ Route::group(['prefix' => 'api'], function() use($middleware) { Route::get('app/settings', 'UserAppSettingsController@get')->middleware($middleware); Route::post('app/settings', 'UserAppSettingsController@store')->middleware($middleware); - Route::group(['prefix' => 'stories'], function () use($middleware) { + Route::group(['prefix' => 'stories'], function () use ($middleware) { Route::get('carousel', 'Stories\StoryApiV1Controller@carousel')->middleware($middleware); Route::get('self-carousel', 'Stories\StoryApiV1Controller@selfCarousel')->middleware($middleware); Route::post('add', 'Stories\StoryApiV1Controller@add')->middleware($middleware); From ed7c3bd888369155ae1876af999e99e1344b11d6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 28 Sep 2024 02:59:38 -0600 Subject: [PATCH 03/20] Update instance config --- config/instance.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/instance.php b/config/instance.php index d1b5b9b61..3538fbf6d 100644 --- a/config/instance.php +++ b/config/instance.php @@ -154,6 +154,7 @@ return [ 'nag' => [ 'enabled' => (bool) env('INSTANCE_NOTIFY_APP_GATEWAY', true), + 'api_key' => env('PIXELFED_PUSHGATEWAY_KEY', false), 'endpoint' => 'push.pixelfed.net', ], ], From dba012840c5656acfd6dfbc4afa6f8a8232ad15f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 28 Sep 2024 03:51:39 -0600 Subject: [PATCH 04/20] Add PushGatewayRefresh command --- app/Console/Commands/PushGatewayRefresh.php | 66 +++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/Console/Commands/PushGatewayRefresh.php diff --git a/app/Console/Commands/PushGatewayRefresh.php b/app/Console/Commands/PushGatewayRefresh.php new file mode 100644 index 000000000..3a15154e6 --- /dev/null +++ b/app/Console/Commands/PushGatewayRefresh.php @@ -0,0 +1,66 @@ +info('Checking Push Notification support...'); + $this->line(' '); + + $currentState = NotificationAppGatewayService::enabled(); + + if($currentState) { + $this->info('Push Notification support is active!'); + return; + } else { + $this->error('Push notification support is NOT active'); + + $action = select( + label: 'Do you want to force re-check?', + options: ['Yes', 'No'], + required: true + ); + + if($action === 'Yes') { + $recheck = NotificationAppGatewayService::forceSupportRecheck(); + 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; + } + } +} From 92ad62e7354c5f821ad83e731e18b2e340d338f2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 28 Sep 2024 03:52:58 -0600 Subject: [PATCH 05/20] Update NotificationAppGatewayService --- app/Console/Commands/PushGatewayRefresh.php | 14 ++- .../NotificationAppGatewayService.php | 112 ++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) 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; + } + } } From df5a9f2659c02a8ef2096e85a4b236cf7ee1e2cd Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sat, 28 Sep 2024 04:09:29 -0600 Subject: [PATCH 06/20] Update nag endpoint --- app/Http/Controllers/Api/ApiV1Dot1Controller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index ac2c1f55a..82929fe68 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -1258,6 +1258,8 @@ class ApiV1Dot1Controller extends Controller { abort_unless((bool) config_cache('pixelfed.oauth_enabled'), 404); - return NotificationAppGatewayService::config(); + return [ + 'active' => NotificationAppGatewayService::enabled(), + ]; } } From 141fc77be99031527190349d081e1a6efd4df1ee Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 00:57:36 -0600 Subject: [PATCH 07/20] Update User model, add notify_enabled --- app/User.php | 2 +- ...93322_add_notify_shares_to_users_table.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php diff --git a/app/User.php b/app/User.php index 30b502308..086e4b0d8 100644 --- a/app/User.php +++ b/app/User.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Passport\HasApiTokens; -use NotificationChannels\Expo\ExpoPushToken; use NotificationChannels\WebPush\HasPushSubscriptions; class User extends Authenticatable @@ -46,6 +45,7 @@ class User extends Authenticatable 'last_active_at', 'register_source', 'expo_token', + 'notify_enabled', 'notify_like', 'notify_follow', 'notify_mention', diff --git a/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php b/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php new file mode 100644 index 000000000..54225d46a --- /dev/null +++ b/database/migrations/2024_09_18_093322_add_notify_shares_to_users_table.php @@ -0,0 +1,28 @@ +boolean('notify_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('notify_enabled'); + }); + } +}; From 24028a59555e19d60949fb609348321940c18eaf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 00:59:53 -0600 Subject: [PATCH 08/20] Update horizon config, add new pushnotify queue --- config/horizon.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/horizon.php b/config/horizon.php index 0a4add6f8..5f7b31c13 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -93,6 +93,7 @@ return [ 'redis:adelete' => 30, 'redis:groups' => 30, 'redis:move' => 30, + 'redis:pushnotify' => 30, ], /* @@ -176,7 +177,7 @@ return [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move', 'pushnotify'], 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), @@ -190,7 +191,7 @@ return [ 'local' => [ 'supervisor-1' => [ 'connection' => 'redis', - 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move'], + 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg', 'groups', 'adelete', 'move', 'pushnotify'], 'balance' => 'auto', 'minProcesses' => 1, 'maxProcesses' => 20, From b29d792c8600e9730dcd4ff9c0dea5d931c64982 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 02:55:44 -0600 Subject: [PATCH 09/20] Add PushNotificationService --- app/Services/PushNotificationService.php | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 app/Services/PushNotificationService.php diff --git a/app/Services/PushNotificationService.php b/app/Services/PushNotificationService.php new file mode 100644 index 000000000..8acb07acc --- /dev/null +++ b/app/Services/PushNotificationService.php @@ -0,0 +1,123 @@ +first(); + if (! $user || $user->status || $user->deleted_at) { + return false; + } + + return Redis::sadd(self::ACTIVE_LIST_KEY.$listId, $memberId); + } + + public static function check($listId, $memberId) + { + return random_int(1, self::LOTTERY_ODDS) === 1 + ? self::isMemberDeepCheck($listId, $memberId) + : self::isMember($listId, $memberId); + } + + public static function isMember($listId, $memberId) + { + try { + return Redis::sismember(self::ACTIVE_LIST_KEY.$listId, $memberId); + } catch (Exception $e) { + return false; + } + } + + public static function isMemberDeepCheck($listId, $memberId) + { + $lock = Cache::lock(self::DEEP_CHECK_KEY.$listId, self::CACHE_LOCK_SECONDS); + + try { + $lock->block(5); + $actualCount = User::whereNull('status')->where('notify_enabled', true)->where('notify_'.$listId, true)->count(); + $cachedCount = self::count($listId); + if ($actualCount != $cachedCount) { + self::warmList($listId); + $user = User::where('notify_enabled', true)->where('profile_id', $memberId)->first(); + + return $user ? (bool) $user->{"notify_{$listId}"} : false; + } else { + return self::isMember($listId, $memberId); + } + } catch (Exception $e) { + Log::error('Failed during deep membership check: '.$e->getMessage()); + + return false; + } finally { + optional($lock)->release(); + } + } + + public static function removeMember($listId, $memberId) + { + return Redis::srem(self::ACTIVE_LIST_KEY.$listId, $memberId); + } + + public static function removeMemberFromAll($memberId) + { + foreach (self::NOTIFY_TYPES as $type) { + self::removeMember($type, $memberId); + } + + return 1; + } + + public static function count($listId) + { + if (! in_array($listId, self::NOTIFY_TYPES)) { + return false; + } + + return Redis::scard(self::ACTIVE_LIST_KEY.$listId); + } + + public static function warmList($listId) + { + if (! in_array($listId, self::NOTIFY_TYPES)) { + return false; + } + $key = self::ACTIVE_LIST_KEY.$listId; + Redis::del($key); + foreach (User::where('notify_'.$listId, true)->cursor() as $acct) { + if ($acct->status || $acct->deleted_at || ! $acct->profile_id || ! $acct->notify_enabled) { + continue; + } + Redis::sadd($key, $acct->profile_id); + } + + return self::count($listId); + } +} From 078ea326c620e4c7d83d89d88949cd384ed780e4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 02:57:46 -0600 Subject: [PATCH 10/20] Update ApiV1Dot1Controller --- .../Controllers/Api/ApiV1Dot1Controller.php | 147 +++++++++++++++--- 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Api/ApiV1Dot1Controller.php b/app/Http/Controllers/Api/ApiV1Dot1Controller.php index 82929fe68..3741390e2 100644 --- a/app/Http/Controllers/Api/ApiV1Dot1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Dot1Controller.php @@ -29,6 +29,7 @@ use App\Services\NetworkTimelineService; use App\Services\NotificationAppGatewayService; use App\Services\ProfileStatusService; use App\Services\PublicTimelineService; +use App\Services\PushNotificationService; use App\Services\StatusService; use App\Services\UserStorageService; use App\Status; @@ -1020,14 +1021,20 @@ class ApiV1Dot1Controller extends Controller return $this->json($account, 200, $rateLimiting ? $limits : []); } - public function getExpoPushNotifications(Request $request) + public function getPushState(Request $request) { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('push'), 403); - abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); $user = $request->user(); + abort_if($user->status, 422, 'Cannot access this resource at this time'); $res = [ - 'expo_token' => (bool) $user->expo_token, + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, 'notify_like' => (bool) $user->notify_like, 'notify_follow' => (bool) $user->notify_follow, 'notify_mention' => (bool) $user->notify_mention, @@ -1037,45 +1044,147 @@ class ApiV1Dot1Controller extends Controller return $this->json($res); } - public function disableExpoPushNotifications(Request $request) + public function disablePush(Request $request) { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('push'), 403); - abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + $request->user()->update([ + 'notify_enabled' => false, 'expo_token' => null, + 'notify_like' => false, + 'notify_follow' => false, + 'notify_mention' => false, + 'notify_comment' => false, ]); - return $this->json(['expo_token' => null]); + PushNotificationService::removeMemberFromAll($request->user()->profile_id); + + $user = $request->user(); + + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, + 'notify_like' => (bool) $user->notify_like, + 'notify_follow' => (bool) $user->notify_follow, + 'notify_mention' => (bool) $user->notify_mention, + 'notify_comment' => (bool) $user->notify_comment, + ]); } - public function updateExpoPushNotifications(Request $request) + public function comparePush(Request $request) { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('push'), 403); - abort_unless(config('services.expo.access_token') && strlen(config('services.expo.access_token')) > 10, 404, 'Push notifications are not supported on this server.'); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + $this->validate($request, [ 'expo_token' => ['required', ExpoPushToken::rule()], + ]); + + $user = $request->user(); + + if (empty($user->expo_token)) { + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'match' => false, + 'has_existing' => false, + ]); + } + + $token = $request->input('expo_token'); + $knownToken = $user->expo_token; + $match = hash_equals($knownToken, $token); + + return $this->json([ + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'username' => (string) $user->username, + 'profile_id' => (string) $user->profile_id, + 'notify_enabled' => (bool) $user->notify_enabled, + 'match' => $match, + 'has_existing' => true, + ]); + } + + public function updatePush(Request $request) + { + abort_unless($request->hasHeader('X-PIXELFED-APP'), 404, 'Not found'); + abort_if(! $request->user() || ! $request->user()->token(), 403); + abort_unless($request->user()->tokenCan('push'), 403); + abort_unless(NotificationAppGatewayService::enabled(), 404, 'Push notifications are not supported on this server.'); + abort_if($request->user()->status, 422, 'Cannot access this resource at this time'); + + $this->validate($request, [ + 'notify_enabled' => 'required', + 'token' => ['required', ExpoPushToken::rule()], 'notify_like' => 'sometimes', 'notify_follow' => 'sometimes', 'notify_mention' => 'sometimes', 'notify_comment' => 'sometimes', ]); - $user = $request->user()->update([ - 'expo_token' => $request->input('expo_token'), - 'notify_like' => $request->has('notify_like') && $request->boolean('notify_like'), - 'notify_follow' => $request->has('notify_follow') && $request->boolean('notify_follow'), - 'notify_mention' => $request->has('notify_mention') && $request->boolean('notify_mention'), - 'notify_comment' => $request->has('notify_comment') && $request->boolean('notify_comment'), + $pid = $request->user()->profile_id; + abort_if(! $pid, 422, 'An error occured'); + $expoToken = $request->input('token'); + + $existing = User::where('profile_id', '!=', $pid)->whereExpoToken($expoToken)->count(); + abort_if($existing, 400, 'Push token is already used by another account'); + + $request->user()->update([ + 'notify_enabled' => $request->boolean('notify_enabled'), + 'expo_token' => $expoToken, ]); + if ($request->filled('notify_like')) { + $request->user()->update(['notify_like' => (bool) $request->boolean('notify_like')]); + $request->boolean('notify_like') == true ? + PushNotificationService::set('like', $pid) : + PushNotificationService::removeMember('like', $pid); + } + if ($request->filled('notify_follow')) { + $request->user()->update(['notify_follow' => (bool) $request->boolean('notify_follow')]); + $request->boolean('notify_follow') == true ? + PushNotificationService::set('follow', $pid) : + PushNotificationService::removeMember('follow', $pid); + } + if ($request->filled('notify_mention')) { + $request->user()->update(['notify_mention' => (bool) $request->boolean('notify_mention')]); + $request->boolean('notify_mention') == true ? + PushNotificationService::set('mention', $pid) : + PushNotificationService::removeMember('mention', $pid); + } + if ($request->filled('notify_comment')) { + $request->user()->update(['notify_comment' => (bool) $request->boolean('notify_comment')]); + $request->boolean('notify_comment') == true ? + PushNotificationService::set('comment', $pid) : + PushNotificationService::removeMember('comment', $pid); + } + + if ($request->boolean('notify_enabled') == false) { + PushNotificationService::removeMemberFromAll($request->user()->profile_id); + } + + $user = $request->user(); + $res = [ - 'expo_token' => (bool) $request->user()->expo_token, - 'notify_like' => (bool) $request->user()->notify_like, - 'notify_follow' => (bool) $request->user()->notify_follow, - 'notify_mention' => (bool) $request->user()->notify_mention, - 'notify_comment' => (bool) $request->user()->notify_comment, + 'version' => PushNotificationService::PUSH_GATEWAY_VERSION, + 'notify_enabled' => (bool) $user->notify_enabled, + 'has_token' => (bool) $user->expo_token, + 'notify_like' => (bool) $user->notify_like, + 'notify_follow' => (bool) $user->notify_follow, + 'notify_mention' => (bool) $user->notify_mention, + 'notify_comment' => (bool) $user->notify_comment, ]; return $this->json($res); From 886d6ec789dfb7a7e4708d8c77113577cc54535f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 02:58:24 -0600 Subject: [PATCH 11/20] Update API routes --- routes/api.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/routes/api.php b/routes/api.php index e996e5485..f3d4b57bb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -268,10 +268,11 @@ Route::group(['prefix' => 'api'], function () use ($middleware) { Route::post('invite/admin/ec', 'AdminInviteController@apiEmailCheck')->middleware('throttle:10,1440'); }); - Route::group(['prefix' => 'expo'], function () use ($middleware) { - Route::get('push-notifications', 'Api\ApiV1Dot1Controller@getExpoPushNotifications')->middleware($middleware); - Route::post('push-notifications/update', 'Api\ApiV1Dot1Controller@updateExpoPushNotifications')->middleware($middleware); - Route::post('push-notifications/disable', 'Api\ApiV1Dot1Controller@disableExpoPushNotifications')->middleware($middleware); + Route::group(['prefix' => 'push'], function () use ($middleware) { + Route::get('state', 'Api\ApiV1Dot1Controller@getPushState')->middleware($middleware); + Route::post('compare', 'Api\ApiV1Dot1Controller@comparePush')->middleware($middleware); + Route::post('update', 'Api\ApiV1Dot1Controller@updatePush')->middleware($middleware); + Route::post('disable', 'Api\ApiV1Dot1Controller@disablePush')->middleware($middleware); }); Route::post('status/create', 'Api\ApiV1Dot1Controller@statusCreate')->middleware($middleware); From ae6fca4d2329e68dd99e527953a271df1f008fde Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 04:02:33 -0600 Subject: [PATCH 12/20] Update NotificationAppGatewayService --- app/Services/NotificationAppGatewayService.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/Services/NotificationAppGatewayService.php b/app/Services/NotificationAppGatewayService.php index 334c08547..c22d2c2e2 100644 --- a/app/Services/NotificationAppGatewayService.php +++ b/app/Services/NotificationAppGatewayService.php @@ -67,14 +67,16 @@ class NotificationAppGatewayService if (! $token || empty($token)) { return false; } - $starts = str_starts_with($token, 'Expo'); - if (! $starts) { + + if (str_starts_with($token, 'ExponentPushToken[') && mb_strlen($token) < 26) { return false; } - if (! str_contains($token, 'PushToken[') || ! str_contains($token, ']')) { + + if (! str_starts_with($token, 'ExponentPushToken[') && ! str_starts_with($token, 'ExpoPushToken[')) { return false; } - if (substr($token, -1) !== ']') { + + if (! str_ends_with($token, ']')) { return false; } @@ -91,7 +93,7 @@ class NotificationAppGatewayService return false; } - $types = ['new_follower', 'like', 'comment', 'share']; + $types = PushNotificationService::NOTIFY_TYPES; if (! $type || empty($type) || ! in_array($type, $types)) { return false; From bd968405876688edda48d21062c5117f78969f0d Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 04:43:58 -0600 Subject: [PATCH 13/20] Add FollowPushNotifyPipeline --- .../FollowPushNotifyPipeline.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php diff --git a/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php new file mode 100644 index 000000000..ee286f5f2 --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/FollowPushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'follow', $this->actor); + } catch (Exception $e) { + return; + } + } +} From 7b4256549a229d76e1fbd7003da37cfb11ceafc0 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 04:45:36 -0600 Subject: [PATCH 14/20] Update FollowPipeline --- app/Jobs/FollowPipeline/FollowPipeline.php | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php index 67733919f..6d30f0fc2 100644 --- a/app/Jobs/FollowPipeline/FollowPipeline.php +++ b/app/Jobs/FollowPipeline/FollowPipeline.php @@ -3,7 +3,13 @@ namespace App\Jobs\FollowPipeline; use App\Follower; +use App\Jobs\PushNotificationPipeline\FollowPushNotifyPipeline; use App\Notification; +use App\Services\AccountService; +use App\Services\FollowerService; +use App\Services\NotificationAppGatewayService; +use App\Services\PushNotificationService; +use App\User; use Cache; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -11,9 +17,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Log; -use Illuminate\Support\Facades\Redis; -use App\Services\AccountService; -use App\Services\FollowerService; class FollowPipeline implements ShouldQueue { @@ -49,16 +52,16 @@ class FollowPipeline implements ShouldQueue $actor = $follower->actor; $target = $follower->target; - if(!$actor || !$target) { + if (! $actor || ! $target) { return; } - if($target->domain || !$target->private_key) { + if ($target->domain || ! $target->private_key) { return; } - Cache::forget('profile:following:' . $actor->id); - Cache::forget('profile:following:' . $target->id); + Cache::forget('profile:following:'.$actor->id); + Cache::forget('profile:following:'.$target->id); FollowerService::add($actor->id, $target->id); @@ -72,9 +75,9 @@ class FollowPipeline implements ShouldQueue $target->save(); AccountService::del($target->id); - if($target->user_id && $target->domain === null) { + if ($target->user_id && $target->domain === null) { try { - $notification = new Notification(); + $notification = new Notification; $notification->profile_id = $target->id; $notification->actor_id = $actor->id; $notification->action = 'follow'; @@ -84,6 +87,15 @@ class FollowPipeline implements ShouldQueue } catch (Exception $e) { Log::error($e); } + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('follow', $target->id)) { + $user = User::whereProfileId($target->id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + FollowPushNotifyPipeline::dispatchSync($user->expo_token, $actor->username); + } + } + } } } } From c95e7577312a855f4e470596ff4411091b470b79 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 05:00:12 -0600 Subject: [PATCH 15/20] Add LikePushNotificationPipeline --- .../LikePushNotifyPipeline.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php diff --git a/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php new file mode 100644 index 000000000..892624b5a --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/LikePushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'like', $this->actor); + } catch (Exception $e) { + return; + } + } +} From bae0632e342cebb3a3bc132cfcff19a510316df4 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 05:10:05 -0600 Subject: [PATCH 16/20] Add MentionPushNotifyPipeline --- .../MentionPushNotifyPipeline.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php diff --git a/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php b/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php new file mode 100644 index 000000000..cad8c6fb5 --- /dev/null +++ b/app/Jobs/PushNotificationPipeline/MentionPushNotifyPipeline.php @@ -0,0 +1,38 @@ +pushToken = $pushToken; + $this->actor = $actor; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + NotificationAppGatewayService::send($this->pushToken, 'mention', $this->actor); + } catch (Exception $e) { + return; + } + } +} From cc897bc42717e06d401f1bd141ef6e8555014889 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 05:12:52 -0600 Subject: [PATCH 17/20] Update AP Inbox --- app/Util/ActivityPub/Inbox.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 6db3589f5..dd9b1578b 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -15,6 +15,7 @@ use App\Jobs\MovePipeline\MoveMigrateFollowersPipeline; use App\Jobs\MovePipeline\ProcessMovePipeline; use App\Jobs\MovePipeline\UnfollowLegacyAccountMovePipeline; use App\Jobs\ProfilePipeline\HandleUpdateActivity; +use App\Jobs\PushNotificationPipeline\MentionPushNotifyPipeline; use App\Jobs\StatusPipeline\RemoteStatusDelete; use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; use App\Jobs\StoryPipeline\StoryExpire; @@ -27,7 +28,9 @@ use App\Notification; use App\Profile; use App\Services\AccountService; use App\Services\FollowerService; +use App\Services\NotificationAppGatewayService; use App\Services\PollService; +use App\Services\PushNotificationService; use App\Services\ReblogService; use App\Services\UserFilterService; use App\Status; @@ -242,7 +245,7 @@ class Inbox $cc = isset($activity['cc']) ? $activity['cc'] : []; if ($activity['type'] == 'Question') { - $this->handlePollCreate(); + //$this->handlePollCreate(); return; } @@ -531,6 +534,15 @@ class Inbox $notification->item_id = $dm->id; $notification->item_type = "App\DirectMessage"; $notification->save(); + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('mention', $profile->id)) { + $user = User::whereProfileId($profile->id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + MentionPushNotifyPipeline::dispatch($user->expo_token, $actor->username)->onQueue('pushnotify'); + } + } + } } } From 873031538de8436c118e01a106daf4ad55e8939a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 30 Sep 2024 05:13:18 -0600 Subject: [PATCH 18/20] Update FollowPipeline --- app/Jobs/FollowPipeline/FollowPipeline.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/FollowPipeline/FollowPipeline.php b/app/Jobs/FollowPipeline/FollowPipeline.php index 6d30f0fc2..0df16206a 100644 --- a/app/Jobs/FollowPipeline/FollowPipeline.php +++ b/app/Jobs/FollowPipeline/FollowPipeline.php @@ -92,7 +92,7 @@ class FollowPipeline implements ShouldQueue if (PushNotificationService::check('follow', $target->id)) { $user = User::whereProfileId($target->id)->first(); if ($user && $user->expo_token && $user->notify_enabled) { - FollowPushNotifyPipeline::dispatchSync($user->expo_token, $actor->username); + FollowPushNotifyPipeline::dispatch($user->expo_token, $actor->username)->onQueue('pushnotify'); } } } From 6889fffbfbd6f09846418b7edeff6260dd528fe3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 1 Oct 2024 23:59:52 -0600 Subject: [PATCH 19/20] Update LikePipeline --- app/Jobs/LikePipeline/LikePipeline.php | 65 +++++++++++++++++--------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index e55c64f80..dc4080d7e 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -2,19 +2,24 @@ namespace App\Jobs\LikePipeline; -use Cache, DB, Log; -use Illuminate\Support\Facades\Redis; -use App\{Like, Notification}; +use App\Jobs\PushNotificationPipeline\LikePushNotifyPipeline; +use App\Like; +use App\Notification; +use App\Services\NotificationAppGatewayService; +use App\Services\PushNotificationService; +use App\Services\StatusService; +use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; +use App\User; +use App\Util\ActivityPub\Helpers; +use DB; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use App\Util\ActivityPub\Helpers; +use Illuminate\Support\Lottery; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; -use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; -use App\Services\StatusService; class LikePipeline implements ShouldQueue { @@ -30,6 +35,7 @@ class LikePipeline implements ShouldQueue public $deleteWhenMissingModels = true; public $timeout = 5; + public $tries = 1; /** @@ -54,34 +60,42 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - if (!$status) { + if (! $status) { // Ignore notifications to deleted statuses return; } - $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); - $status->save(); + Lottery::odds(1, 20) + ->winner(function () use ($status) { + $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); + $status->save(); + }) + ->loser(function () use ($status) { + $status->likes_count = $status->likes_count + 1; + $status->save(); + }) + ->choose(); StatusService::refresh($status->id); - if($status->url && $actor->domain == null) { + if ($status->url && $actor->domain == null) { return $this->remoteLikeDeliver(); } $exists = Notification::whereProfileId($status->profile_id) - ->whereActorId($actor->id) - ->whereAction('like') - ->whereItemId($status->id) - ->whereItemType('App\Status') - ->count(); + ->whereActorId($actor->id) + ->whereAction('like') + ->whereItemId($status->id) + ->whereItemType('App\Status') + ->count(); - if ($actor->id === $status->profile_id || $exists !== 0) { + if ($actor->id === $status->profile_id || $exists) { return true; } - if($status->uri === null && $status->object_url === null && $status->url === null) { + if ($status->uri === null && $status->object_url === null && $status->url === null) { try { - $notification = new Notification(); + $notification = new Notification; $notification->profile_id = $status->profile_id; $notification->actor_id = $actor->id; $notification->action = 'like'; @@ -91,6 +105,15 @@ class LikePipeline implements ShouldQueue } catch (Exception $e) { } + + if (NotificationAppGatewayService::enabled()) { + if (PushNotificationService::check('like', $status->profile_id)) { + $user = User::whereProfileId($status->profile_id)->first(); + if ($user && $user->expo_token && $user->notify_enabled) { + LikePushNotifyPipeline::dispatchSync($user->expo_token, $actor->username); + } + } + } } } @@ -100,9 +123,9 @@ class LikePipeline implements ShouldQueue $status = $this->like->status; $actor = $this->like->actor; - $fractal = new Fractal\Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Fractal\Resource\Item($like, new LikeTransformer()); + $fractal = new Fractal\Manager; + $fractal->setSerializer(new ArraySerializer); + $resource = new Fractal\Resource\Item($like, new LikeTransformer); $activity = $fractal->createData($resource)->toArray(); $url = $status->profile->sharedInbox ?? $status->profile->inbox_url; From b995af15f68dae543be43a2fb2dad5b107b8118f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 2 Oct 2024 01:09:51 -0600 Subject: [PATCH 20/20] Update LikePipeline --- app/Jobs/LikePipeline/LikePipeline.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index dc4080d7e..7dbd71da2 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -11,13 +11,11 @@ use App\Services\StatusService; use App\Transformer\ActivityPub\Verb\Like as LikeTransformer; use App\User; use App\Util\ActivityPub\Helpers; -use DB; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Lottery; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; @@ -65,17 +63,6 @@ class LikePipeline implements ShouldQueue return; } - Lottery::odds(1, 20) - ->winner(function () use ($status) { - $status->likes_count = DB::table('likes')->whereStatusId($status->id)->count(); - $status->save(); - }) - ->loser(function () use ($status) { - $status->likes_count = $status->likes_count + 1; - $status->save(); - }) - ->choose(); - StatusService::refresh($status->id); if ($status->url && $actor->domain == null) {