From e28a4265c5b7228033b94e897d7643c1741e43db Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 00:30:41 +0000 Subject: [PATCH 1/9] API: First steps to support subscriptions --- database.sql | 26 ++++- doc/API-Mastodon.md | 10 +- doc/database.md | 1 + doc/database/db_subscription.md | 42 ++++++++ src/DI.php | 8 ++ src/Factory/Api/Mastodon/Subscription.php | 41 ++++++++ src/Model/Subscription.php | 66 +++++++++++++ .../Api/Mastodon/Accounts/Followers.php | 2 - .../Api/Mastodon/Accounts/Following.php | 2 - src/Module/Api/Mastodon/Blocks.php | 2 - src/Module/Api/Mastodon/Favourited.php | 2 - src/Module/Api/Mastodon/Lists/Accounts.php | 2 - src/Module/Api/Mastodon/Mutes.php | 2 - src/Module/Api/Mastodon/PushSubscription.php | 97 +++++++++++++++++++ src/Object/Api/Mastodon/Subscription.php | 63 ++++++++++++ src/Util/Crypto.php | 43 ++++++++ static/dbstructure.config.php | 25 ++++- static/routes.config.php | 2 +- 18 files changed, 416 insertions(+), 20 deletions(-) create mode 100644 doc/database/db_subscription.md create mode 100644 src/Factory/Api/Mastodon/Subscription.php create mode 100644 src/Model/Subscription.php create mode 100644 src/Module/Api/Mastodon/PushSubscription.php create mode 100644 src/Object/Api/Mastodon/Subscription.php diff --git a/database.sql b/database.sql index 4d444cbe50..eff8f83faf 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2021.09-dev (Siberian Iris) --- DB_UPDATE_VERSION 1433 +-- DB_UPDATE_VERSION 1434 -- ------------------------------------------ @@ -1472,6 +1472,30 @@ CREATE TABLE IF NOT EXISTS `storage` ( PRIMARY KEY(`id`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Data stored by Database storage backend'; +-- +-- TABLE subscription +-- +CREATE TABLE IF NOT EXISTS `subscription` ( + `id` int unsigned NOT NULL auto_increment COMMENT 'Auto incremented image data id', + `application-id` int unsigned NOT NULL COMMENT '', + `uid` mediumint unsigned NOT NULL COMMENT 'Owner User id', + `endpoint` varchar(511) COMMENT 'Endpoint URL', + `pubkey` varchar(127) COMMENT 'User agent public key', + `secret` varchar(32) COMMENT 'Auth secret', + `follow` boolean COMMENT '', + `favourite` boolean COMMENT '', + `reblog` boolean COMMENT '', + `mention` boolean COMMENT '', + `poll` boolean COMMENT '', + `follow_request` boolean COMMENT '', + `status` boolean COMMENT '', + PRIMARY KEY(`id`), + UNIQUE INDEX `application-id_uid` (`application-id`,`uid`), + INDEX `uid_application-id` (`uid`,`application-id`), + FOREIGN KEY (`application-id`) REFERENCES `application` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Push Subscription for the API'; + -- -- TABLE userd -- diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 006b8da473..dd82a5144d 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -93,7 +93,12 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`POST /api/v1/notifications/clear`](https://docs.joinmastodon.org/methods/notifications/) - [`POST /api/v1/notifications/:id/dismiss`](https://docs.joinmastodon.org/methods/notifications/) - [`GET /api/v1/preferences`](https://docs.joinmastodon.org/methods/accounts/preferences/) +- [`DELETE /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) +- [`GET /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) +- [`PUSH /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`GET /api/v1/scheduled_statuses`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) +- [`DELETE /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) +- [`GET /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/search`](https://docs.joinmastodon.org/methods/search/) - [`POST /api/v1/statuses`](https://docs.joinmastodon.org/methods/statuses/) - [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/) @@ -173,13 +178,8 @@ They refer to features or data that don't exist in Friendica yet. - [`POST /api/v1/markers`](https://docs.joinmastodon.org/methods/timelines/markers/) - [`GET /api/v1/polls/:id`](https://docs.joinmastodon.org/methods/statuses/polls/) - [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/) -- [`DELETE /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) -- [`GET /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) -- [`PUSH /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`PUT /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`POST /api/v1/reports`](https://docs.joinmastodon.org/methods/accounts/reports/) -- [`GET /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`PUT /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) -- [`DELETE /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/streaming`](https://docs.joinmastodon.org/methods/timelines/streaming/) - [`DELETE /api/v1/suggestions/:id`](https://docs.joinmastodon.org/methods/accounts/suggestions/) diff --git a/doc/database.md b/doc/database.md index 194f30eb6e..bc690c6cd2 100644 --- a/doc/database.md +++ b/doc/database.md @@ -65,6 +65,7 @@ Database Tables | [search](help/database/db_search) | | | [session](help/database/db_session) | web session storage | | [storage](help/database/db_storage) | Data stored by Database storage backend | +| [subscription](help/database/db_subscription) | Push Subscription for the API | | [tag](help/database/db_tag) | tags and mentions | | [user](help/database/db_user) | The local users | | [user-contact](help/database/db_user-contact) | User specific public contact data | diff --git a/doc/database/db_subscription.md b/doc/database/db_subscription.md new file mode 100644 index 0000000000..8bcf4f2379 --- /dev/null +++ b/doc/database/db_subscription.md @@ -0,0 +1,42 @@ +Table subscription +=========== + +Push Subscription for the API + +Fields +------ + +| Field | Description | Type | Null | Key | Default | Extra | +| -------------- | ------------------------------ | ------------------ | ---- | --- | ------- | -------------- | +| id | Auto incremented image data id | int unsigned | NO | PRI | NULL | auto_increment | +| application-id | | int unsigned | NO | | NULL | | +| uid | Owner User id | mediumint unsigned | NO | | NULL | | +| endpoint | Endpoint URL | varchar(511) | YES | | NULL | | +| pubkey | User agent public key | varchar(127) | YES | | NULL | | +| secret | Auth secret | varchar(32) | YES | | NULL | | +| follow | | boolean | YES | | NULL | | +| favourite | | boolean | YES | | NULL | | +| reblog | | boolean | YES | | NULL | | +| mention | | boolean | YES | | NULL | | +| poll | | boolean | YES | | NULL | | +| follow_request | | boolean | YES | | NULL | | +| status | | boolean | YES | | NULL | | + +Indexes +------------ + +| Name | Fields | +| ------------------ | --------------------------- | +| PRIMARY | id | +| application-id_uid | UNIQUE, application-id, uid | +| uid_application-id | uid, application-id | + +Foreign Keys +------------ + +| Field | Target Table | Target Field | +|-------|--------------|--------------| +| application-id | [application](help/database/db_application) | id | +| uid | [user](help/database/db_user) | uid | + +Return to [database documentation](help/database) diff --git a/src/DI.php b/src/DI.php index 0bfaacf89a..02620ea11e 100644 --- a/src/DI.php +++ b/src/DI.php @@ -319,6 +319,14 @@ abstract class DI return self::$dice->create(Factory\Api\Mastodon\ScheduledStatus::class); } + /** + * @return Factory\Api\Mastodon\Subscription + */ + public static function mstdnSubscription() + { + return self::$dice->create(Factory\Api\Mastodon\Subscription::class); + } + /** * @return Factory\Api\Mastodon\ListEntity */ diff --git a/src/Factory/Api/Mastodon/Subscription.php b/src/Factory/Api/Mastodon/Subscription.php new file mode 100644 index 0000000000..3b8f343e17 --- /dev/null +++ b/src/Factory/Api/Mastodon/Subscription.php @@ -0,0 +1,41 @@ +. + * + */ + +namespace Friendica\Factory\Api\Mastodon; + +use Friendica\BaseFactory; +use Friendica\Database\DBA; +use Friendica\Model\Subscription as ModelSubscription; + +class Subscription extends BaseFactory +{ + /** + * @param int $applicationid Application Id + * @param int $uid Item user + * + * @return \Friendica\Object\Api\Mastodon\Status + */ + public function createForApplicationIdAndUserId(int $applicationid, int $uid): \Friendica\Object\Api\Mastodon\Subscription + { + $subscription = DBA::selectFirst('subscription', ['application-id' => $applicationid, 'uid' => $uid]); + return new \Friendica\Object\Api\Mastodon\Subscription($subscription, ModelSubscription::getVapidKey()); + } +} diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php new file mode 100644 index 0000000000..173d5a9ebf --- /dev/null +++ b/src/Model/Subscription.php @@ -0,0 +1,66 @@ +. + * + */ + +namespace Friendica\Model; + +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Util\Crypto; + +class Subscription +{ + /** + * Insert an Subscription record + * + * @param array $fields subscription fields + * + * @return bool result of replace + */ + public static function replace(array $fields) + { + return DBA::replace('subscription', $fields); + } + + /** + * Delete a subscription record + * @param int $applicationid + * @param int $uid + * @return bool + */ + public static function delete(int $applicationid, int $uid) + { + return DBA::delete('subscription', ['application-id' => $applicationid, 'uid' => $uid]); + } + + /** + * Fetch a VAPID key + * @return string + */ + public static function getVapidKey():string + { + $keypair = DI::config()->get('system', 'ec_keypair'); + if (empty($keypair)) { + $keypair = Crypto::newECKeypair(); + DI::config()->set('system', 'ec_keypair', $keypair); + } + return $keypair['vapid']; + } +} diff --git a/src/Module/Api/Mastodon/Accounts/Followers.php b/src/Module/Api/Mastodon/Accounts/Followers.php index c495d6ca1e..e1f864acbe 100644 --- a/src/Module/Api/Mastodon/Accounts/Followers.php +++ b/src/Module/Api/Mastodon/Accounts/Followers.php @@ -49,8 +49,6 @@ class Followers extends BaseApi DI::mstdnError()->RecordNotFound(); } - // @todo provide HTTP link header - $request = self::getRequest([ 'max_id' => 0, // Return results older than this id 'since_id' => 0, // Return results newer than this id diff --git a/src/Module/Api/Mastodon/Accounts/Following.php b/src/Module/Api/Mastodon/Accounts/Following.php index b6a8f7f75c..e2b963e0c3 100644 --- a/src/Module/Api/Mastodon/Accounts/Following.php +++ b/src/Module/Api/Mastodon/Accounts/Following.php @@ -49,8 +49,6 @@ class Following extends BaseApi DI::mstdnError()->RecordNotFound(); } - // @todo provide HTTP link header - $request = self::getRequest([ 'max_id' => 0, // Return results older than this id 'since_id' => 0, // Return results newer than this id diff --git a/src/Module/Api/Mastodon/Blocks.php b/src/Module/Api/Mastodon/Blocks.php index 37ab61af52..305914c1aa 100644 --- a/src/Module/Api/Mastodon/Blocks.php +++ b/src/Module/Api/Mastodon/Blocks.php @@ -49,8 +49,6 @@ class Blocks extends BaseApi DI::mstdnError()->RecordNotFound(); } - // @todo provide HTTP link header - $request = self::getRequest([ 'max_id' => 0, // Return results older than this id 'since_id' => 0, // Return results newer than this id diff --git a/src/Module/Api/Mastodon/Favourited.php b/src/Module/Api/Mastodon/Favourited.php index 239257e833..31e760d3b8 100644 --- a/src/Module/Api/Mastodon/Favourited.php +++ b/src/Module/Api/Mastodon/Favourited.php @@ -43,8 +43,6 @@ class Favourited extends BaseApi self::checkAllowedScope(self::SCOPE_READ); $uid = self::getCurrentUserID(); - // @todo provide HTTP link header - $request = self::getRequest([ 'limit' => 20, // Maximum number of results to return. Defaults to 20. 'min_id' => 0, // Return results immediately newer than id diff --git a/src/Module/Api/Mastodon/Lists/Accounts.php b/src/Module/Api/Mastodon/Lists/Accounts.php index 013a9f4aa9..6e11235630 100644 --- a/src/Module/Api/Mastodon/Lists/Accounts.php +++ b/src/Module/Api/Mastodon/Lists/Accounts.php @@ -62,8 +62,6 @@ class Accounts extends BaseApi DI::mstdnError()->RecordNotFound(); } - // @todo provide HTTP link header - $request = self::getRequest([ 'max_id' => 0, // Return results older than this id 'since_id' => 0, // Return results newer than this id diff --git a/src/Module/Api/Mastodon/Mutes.php b/src/Module/Api/Mastodon/Mutes.php index ea890d9fdb..7939da114b 100644 --- a/src/Module/Api/Mastodon/Mutes.php +++ b/src/Module/Api/Mastodon/Mutes.php @@ -49,8 +49,6 @@ class Mutes extends BaseApi DI::mstdnError()->RecordNotFound(); } - // @todo provide HTTP link header - $request = self::getRequest([ 'max_id' => 0, // Return results older than this id 'since_id' => 0, // Return results newer than this id diff --git a/src/Module/Api/Mastodon/PushSubscription.php b/src/Module/Api/Mastodon/PushSubscription.php new file mode 100644 index 0000000000..68b1b46b13 --- /dev/null +++ b/src/Module/Api/Mastodon/PushSubscription.php @@ -0,0 +1,97 @@ +. + * + */ + +namespace Friendica\Module\Api\Mastodon; + +use Friendica\App\Router; +use Friendica\Core\Logger; +use Friendica\Core\System; +use Friendica\DI; +use Friendica\Model\Subscription; +use Friendica\Module\BaseApi; + +/** + * @see https://docs.joinmastodon.org/methods/notifications/push/ + */ +class PushSubscription extends BaseApi +{ + public static function post(array $parameters = []) + { + self::checkAllowedScope(self::SCOPE_PUSH); + $uid = self::getCurrentUserID(); + $application = self::getCurrentApplication(); + + $request = self::getRequest([ + 'subscription' => [], + 'data' => [], + ]); + + $subscription = [ + 'application-id' => $application['id'], + 'uid' => $uid, + 'endpoint' => $request['subscription']['endpoint'] ?? '', + 'pubkey' => $request['subscription']['keys']['p256dh'] ?? '', + 'secret' => $request['subscription']['keys']['auth'] ?? '', + 'follow' => $request['data']['alerts']['follow'] ?? false, + 'favourite' => $request['data']['alerts']['favourite'] ?? false, + 'reblog' => $request['data']['alerts']['reblog'] ?? false, + 'mention' => $request['data']['alerts']['mention'] ?? false, + 'poll' => $request['data']['alerts']['poll'] ?? false, + 'follow_request' => $request['data']['alerts']['follow_request'] ?? false, + 'status' => $request['data']['alerts']['status'] ?? false, + ]; + + $ret = Subscription::replace($subscription); + + Logger::info('Subscription stored', ['ret' => $ret, 'subscription' => $subscription]); + + return DI::mstdnSubscription()->createForApplicationIdAndUserId($application['id'], $uid)->toArray(); + } + + public static function put(array $parameters = []) + { + self::checkAllowedScope(self::SCOPE_PUSH); + $uid = self::getCurrentUserID(); + $application = self::getCurrentApplication(); + + self::unsupported(Router::PUT); + } + + public static function delete(array $parameters = []) + { + self::checkAllowedScope(self::SCOPE_PUSH); + $uid = self::getCurrentUserID(); + $application = self::getCurrentApplication(); + + Subscription::delete($application['id'], $uid); + + System::jsonExit([]); + } + + public static function rawContent(array $parameters = []) + { + self::checkAllowedScope(self::SCOPE_PUSH); + $uid = self::getCurrentUserID(); + $application = self::getCurrentApplication(); + + return DI::mstdnSubscription()->createForApplicationIdAndUserId($application['id'], $uid)->toArray(); + } +} diff --git a/src/Object/Api/Mastodon/Subscription.php b/src/Object/Api/Mastodon/Subscription.php new file mode 100644 index 0000000000..dcd0a6982f --- /dev/null +++ b/src/Object/Api/Mastodon/Subscription.php @@ -0,0 +1,63 @@ +. + * + */ + +namespace Friendica\Object\Api\Mastodon; + +use Friendica\BaseDataTransferObject; + +/** + * Class Subscription + * + * @see https://docs.joinmastodon.org/entities/pushsubscription + */ +class Subscription extends BaseDataTransferObject +{ + /** @var string */ + protected $id; + /** @var string|null (URL)*/ + protected $endpoint; + /** @var array */ + protected $alerts; + /** @var string */ + protected $server_key; + + /** + * Creates a subscription record from an item record. + * + * @param array $subscription + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function __construct(array $subscription, string $vapid) + { + $this->id = (string)$subscription['id']; + $this->endpoint = $subscription['endpoint']; + $this->alerts = [ + 'follow' => $subscription['follow'], + 'favourite' => $subscription['favourite'], + 'reblog' => $subscription['reblog'], + 'mention' => $subscription['mention'], + 'mention' => $subscription['mention'], + 'poll' => $subscription['poll'], + ]; + + $this->server_key = $vapid; + } +} diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index 0187079e39..1ff0e19c3c 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -25,6 +25,7 @@ use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\DI; +use ParagonIE\ConstantTime\Base64UrlSafe; use phpseclib\Crypt\RSA; use phpseclib\Math\BigInteger; @@ -150,6 +151,48 @@ class Crypto return $response; } + /** + * Create a new elliptic curve key pair + * + * @return array with the elements "prvkey", "vapid" and "pubkey" + */ + public static function newECKeypair() + { + $openssl_options = [ + 'curve_name' => 'prime256v1', + 'private_key_type' => OPENSSL_KEYTYPE_EC + ]; + + $conf = DI::config()->get('system', 'openssl_conf_file'); + if ($conf) { + $openssl_options['config'] = $conf; + } + $result = openssl_pkey_new($openssl_options); + + if (empty($result)) { + Logger::notice('new_keypair: failed'); + return []; + } + + $response = ['prvkey' => '', 'pubkey' => '', 'vapid' => '']; + + // Get private key + openssl_pkey_export($result, $response['prvkey']); + + // Get public key + $pkey = openssl_pkey_get_details($result); + $response['pubkey'] = $pkey['key']; + + // Create VAPID key + // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/Utils.php#L60-L62 + $hexString = '04'; + $hexString .= str_pad(bin2hex($pkey['ec']['x']), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(bin2hex($pkey['ec']['y']), 64, '0', STR_PAD_LEFT); + $response['vapid'] = Base64UrlSafe::encode(hex2bin($hexString)); + + return $response; + } + /** * Encrypt a string with 'aes-256-cbc' cipher method. * diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index f820401a19..6f455d14e9 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -55,7 +55,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1433); + define('DB_UPDATE_VERSION', 1434); } return [ @@ -1492,6 +1492,29 @@ return [ "PRIMARY" => ["id"] ] ], + "subscription" => [ + "comment" => "Push Subscription for the API", + "fields" => [ + "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "Auto incremented image data id"], + "application-id" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["application" => "id"], "comment" => ""], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner User id"], + "endpoint" => ["type" => "varchar(511)", "comment" => "Endpoint URL"], + "pubkey" => ["type" => "varchar(127)", "comment" => "User agent public key"], + "secret" => ["type" => "varchar(32)", "comment" => "Auth secret"], + "follow" => ["type" => "boolean", "comment" => ""], + "favourite" => ["type" => "boolean", "comment" => ""], + "reblog" => ["type" => "boolean", "comment" => ""], + "mention" => ["type" => "boolean", "comment" => ""], + "poll" => ["type" => "boolean", "comment" => ""], + "follow_request" => ["type" => "boolean", "comment" => ""], + "status" => ["type" => "boolean", "comment" => ""], + ], + "indexes" => [ + "PRIMARY" => ["id"], + "application-id_uid" => ["UNIQUE", "application-id", "uid"], + "uid_application-id" => ["uid", "application-id"], + ] + ], "userd" => [ "comment" => "Deleted usernames", "fields" => [ diff --git a/static/routes.config.php b/static/routes.config.php index ac4ec53917..f47051e321 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -126,7 +126,7 @@ return [ '/polls/{id:\d+}' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not supported '/polls/{id:\d+}/votes' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/preferences' => [Module\Api\Mastodon\Preferences::class, [R::GET ]], - '/push/subscription' => [Module\Api\Mastodon\Unimplemented::class, [R::GET, R::POST, R::PUT, R::DELETE]], // not supported + '/push/subscription' => [Module\Api\Mastodon\PushSubscription::class, [R::GET, R::POST, R::PUT, R::DELETE]], '/reports' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/scheduled_statuses' => [Module\Api\Mastodon\ScheduledStatuses::class, [R::GET ]], '/scheduled_statuses/{id:\d+}' => [Module\Api\Mastodon\ScheduledStatuses::class, [R::GET, R::PUT, R::DELETE]], From 167a12ef54c8caa263728a336afde55202067643 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 00:43:07 +0000 Subject: [PATCH 2/9] Styling --- src/Model/Subscription.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index 173d5a9ebf..fac585e992 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -41,9 +41,9 @@ class Subscription /** * Delete a subscription record - * @param int $applicationid - * @param int $uid - * @return bool + * @param int $applicationid + * @param int $uid + * @return bool */ public static function delete(int $applicationid, int $uid) { @@ -54,7 +54,7 @@ class Subscription * Fetch a VAPID key * @return string */ - public static function getVapidKey():string + public static function getVapidKey(): string { $keypair = DI::config()->get('system', 'ec_keypair'); if (empty($keypair)) { From 8b6ace6629ab4fe229c4c35ff388efe320602a16 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 06:45:48 +0000 Subject: [PATCH 3/9] Don't continue when key couldn't be created --- src/Util/Crypto.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index 1ff0e19c3c..85efb9e8ae 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -21,6 +21,7 @@ namespace Friendica\Util; +use Exception; use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\System; @@ -170,8 +171,7 @@ class Crypto $result = openssl_pkey_new($openssl_options); if (empty($result)) { - Logger::notice('new_keypair: failed'); - return []; + throw new Exception('Key creation failed'); } $response = ['prvkey' => '', 'pubkey' => '', 'vapid' => '']; From 74f3cbc3835c742c48723ea7b0360504ccdfebe1 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 07:28:26 +0000 Subject: [PATCH 4/9] Improved error check, added "put" --- doc/API-Mastodon.md | 2 +- src/Factory/Api/Mastodon/Subscription.php | 2 +- src/Model/Subscription.php | 45 +++++++++++++++++++- src/Module/Api/Mastodon/PushSubscription.php | 38 +++++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index dd82a5144d..cec90bea4c 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -96,6 +96,7 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en - [`DELETE /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`GET /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`PUSH /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) +- [`PUT /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`GET /api/v1/scheduled_statuses`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`DELETE /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) @@ -178,7 +179,6 @@ They refer to features or data that don't exist in Friendica yet. - [`POST /api/v1/markers`](https://docs.joinmastodon.org/methods/timelines/markers/) - [`GET /api/v1/polls/:id`](https://docs.joinmastodon.org/methods/statuses/polls/) - [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/) -- [`PUT /api/v1/push/subscription`](https://docs.joinmastodon.org/methods/notifications/push/) - [`POST /api/v1/reports`](https://docs.joinmastodon.org/methods/accounts/reports/) - [`PUT /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/streaming`](https://docs.joinmastodon.org/methods/timelines/streaming/) diff --git a/src/Factory/Api/Mastodon/Subscription.php b/src/Factory/Api/Mastodon/Subscription.php index 3b8f343e17..08dbcf0d17 100644 --- a/src/Factory/Api/Mastodon/Subscription.php +++ b/src/Factory/Api/Mastodon/Subscription.php @@ -35,7 +35,7 @@ class Subscription extends BaseFactory */ public function createForApplicationIdAndUserId(int $applicationid, int $uid): \Friendica\Object\Api\Mastodon\Subscription { - $subscription = DBA::selectFirst('subscription', ['application-id' => $applicationid, 'uid' => $uid]); + $subscription = DBA::selectFirst('subscription', [], ['application-id' => $applicationid, 'uid' => $uid]); return new \Friendica\Object\Api\Mastodon\Subscription($subscription, ModelSubscription::getVapidKey()); } } diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index fac585e992..8d3da36ab3 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -28,7 +28,48 @@ use Friendica\Util\Crypto; class Subscription { /** - * Insert an Subscription record + * Select a subscription record exists + * + * @param int $applicationid + * @param int $uid + * @param array $fields + * + * @return bool Does it exist? + */ + public static function select(int $applicationid, int $uid, array $fields = []) + { + return DBA::selectFirst('subscription', $fields, ['application-id' => $applicationid, 'uid' => $uid]); + } + + /** + * Check if a subscription record exists + * + * @param int $applicationid + * @param int $uid + * + * @return bool Does it exist? + */ + public static function exists(int $applicationid, int $uid) + { + return DBA::exists('subscription', ['application-id' => $applicationid, 'uid' => $uid]); + } + + /** + * Update a subscription record + * + * @param int $applicationid + * @param int $uid + * @param array $fields subscription fields + * + * @return bool result of update + */ + public static function update(int $applicationid, int $uid, array $fields) + { + return DBA::update('subscription', $fields, ['application-id' => $applicationid, 'uid' => $uid]); + } + + /** + * Insert or replace a subscription record * * @param array $fields subscription fields * @@ -41,6 +82,7 @@ class Subscription /** * Delete a subscription record + * * @param int $applicationid * @param int $uid * @return bool @@ -52,6 +94,7 @@ class Subscription /** * Fetch a VAPID key + * * @return string */ public static function getVapidKey(): string diff --git a/src/Module/Api/Mastodon/PushSubscription.php b/src/Module/Api/Mastodon/PushSubscription.php index 68b1b46b13..411d5391f5 100644 --- a/src/Module/Api/Mastodon/PushSubscription.php +++ b/src/Module/Api/Mastodon/PushSubscription.php @@ -21,7 +21,6 @@ namespace Friendica\Module\Api\Mastodon; -use Friendica\App\Router; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\DI; @@ -72,7 +71,31 @@ class PushSubscription extends BaseApi $uid = self::getCurrentUserID(); $application = self::getCurrentApplication(); - self::unsupported(Router::PUT); + $request = self::getRequest([ + 'data' => [], + ]); + + $subscription = Subscription::select($application['id'], $uid, ['id']); + if (empty($subscription)) { + Logger::info('Subscription not found', ['application-id' => $application['id'], 'uid' => $uid]); + DI::mstdnError()->RecordNotFound(); + } + + $fields = [ + 'follow' => $request['data']['alerts']['follow'] ?? false, + 'favourite' => $request['data']['alerts']['favourite'] ?? false, + 'reblog' => $request['data']['alerts']['reblog'] ?? false, + 'mention' => $request['data']['alerts']['mention'] ?? false, + 'poll' => $request['data']['alerts']['poll'] ?? false, + 'follow_request' => $request['data']['alerts']['follow_request'] ?? false, + 'status' => $request['data']['alerts']['status'] ?? false, + ]; + + $ret = Subscription::update($application['id'], $uid, $fields); + + Logger::info('Subscription updated', ['result' => $ret, 'application-id' => $application['id'], 'uid' => $uid, 'fields' => $fields]); + + return DI::mstdnSubscription()->createForApplicationIdAndUserId($application['id'], $uid)->toArray(); } public static function delete(array $parameters = []) @@ -81,7 +104,9 @@ class PushSubscription extends BaseApi $uid = self::getCurrentUserID(); $application = self::getCurrentApplication(); - Subscription::delete($application['id'], $uid); + $ret = Subscription::delete($application['id'], $uid); + + Logger::info('Subscription deleted', ['result' => $ret, 'application-id' => $application['id'], 'uid' => $uid]); System::jsonExit([]); } @@ -92,6 +117,13 @@ class PushSubscription extends BaseApi $uid = self::getCurrentUserID(); $application = self::getCurrentApplication(); + if (!Subscription::exists($application['id'], $uid)) { + Logger::info('Subscription not found', ['application-id' => $application['id'], 'uid' => $uid]); + DI::mstdnError()->RecordNotFound(); + } + + Logger::info('Fetch subscription', ['application-id' => $application['id'], 'uid' => $uid]); + return DI::mstdnSubscription()->createForApplicationIdAndUserId($application['id'], $uid)->toArray(); } } From 5056376902f8f665455647f6152adc95e107499d Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 12:57:29 +0000 Subject: [PATCH 5/9] We need to create a vapid public and private key --- src/Model/Subscription.php | 6 +++++- src/Util/Crypto.php | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index 8d3da36ab3..fa93eb5218 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -19,6 +19,10 @@ * */ + /** + * @see https://github.com/web-push-libs/web-push-php + * Possibly we should simply use this. + */ namespace Friendica\Model; use Friendica\Database\DBA; @@ -104,6 +108,6 @@ class Subscription $keypair = Crypto::newECKeypair(); DI::config()->set('system', 'ec_keypair', $keypair); } - return $keypair['vapid']; + return $keypair['vapid-public']; } } diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index 85efb9e8ae..b3ae2d69b8 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -155,7 +155,7 @@ class Crypto /** * Create a new elliptic curve key pair * - * @return array with the elements "prvkey", "vapid" and "pubkey" + * @return array with the elements "prvkey", "pubkey", "vapid-public" and "vapid-private" */ public static function newECKeypair() { @@ -174,7 +174,7 @@ class Crypto throw new Exception('Key creation failed'); } - $response = ['prvkey' => '', 'pubkey' => '', 'vapid' => '']; + $response = ['prvkey' => '', 'pubkey' => '']; // Get private key openssl_pkey_export($result, $response['prvkey']); @@ -183,12 +183,15 @@ class Crypto $pkey = openssl_pkey_get_details($result); $response['pubkey'] = $pkey['key']; - // Create VAPID key + // Create VAPID keys // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/Utils.php#L60-L62 $hexString = '04'; $hexString .= str_pad(bin2hex($pkey['ec']['x']), 64, '0', STR_PAD_LEFT); $hexString .= str_pad(bin2hex($pkey['ec']['y']), 64, '0', STR_PAD_LEFT); - $response['vapid'] = Base64UrlSafe::encode(hex2bin($hexString)); + $response['vapid-public'] = Base64UrlSafe::encode(hex2bin($hexString)); + + // @see https://github.com/web-push-libs/web-push-php/blob/256a18b2a2411469c94943725fb6eccb9681bd75/src/VAPID.php + $response['vapid-private'] = Base64UrlSafe::encode(hex2bin(str_pad(bin2hex($pkey['ec']['d']), 64, '0', STR_PAD_LEFT))); return $response; } From a3584c0bd30560ad10e33d51467ab223bdb89a82 Mon Sep 17 00:00:00 2001 From: Michael Vogel Date: Sun, 15 Aug 2021 17:27:45 +0200 Subject: [PATCH 6/9] Update src/Model/Subscription.php Co-authored-by: Hypolite Petovan --- src/Model/Subscription.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index fa93eb5218..4e176c8fa3 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -23,6 +23,7 @@ * @see https://github.com/web-push-libs/web-push-php * Possibly we should simply use this. */ + namespace Friendica\Model; use Friendica\Database\DBA; From 19f3cad56c365a6be362aca811faa07cb3a6bdf4 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 16:18:25 +0000 Subject: [PATCH 7/9] Preparation for creating push notification --- src/Factory/Api/Mastodon/Notification.php | 23 +++-------------- src/Model/Notification.php | 31 +++++++++++++++++++++++ src/Model/Post/UserNotification.php | 19 ++++++++++++-- src/Model/Subscription.php | 23 +++++++++++++++++ 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/Factory/Api/Mastodon/Notification.php b/src/Factory/Api/Mastodon/Notification.php index 2376e58cc9..82ef67d618 100644 --- a/src/Factory/Api/Mastodon/Notification.php +++ b/src/Factory/Api/Mastodon/Notification.php @@ -23,10 +23,7 @@ namespace Friendica\Factory\Api\Mastodon; use Friendica\BaseFactory; use Friendica\Database\Database; -use Friendica\Model\Contact; -use Friendica\Model\Post; -use Friendica\Model\Verb; -use Friendica\Protocol\Activity; +use Friendica\Model\Notification as ModelNotification; use Psr\Log\LoggerInterface; class Notification extends BaseFactory @@ -62,22 +59,8 @@ class Notification extends BaseFactory status = Someone you enabled notifications for has posted a status */ - if (($notification['vid'] == Verb::getID(Activity::FOLLOW)) && ($notification['type'] == Post\UserNotification::NOTIF_NONE)) { - $contact = Contact::getById($notification['actor-id'], ['pending']); - $type = $contact['pending'] ? $type = 'follow_request' : 'follow'; - } elseif (($notification['vid'] == Verb::getID(Activity::ANNOUNCE)) && - in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { - $type = 'reblog'; - } elseif (in_array($notification['vid'], [Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]) && - in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { - $type = 'favourite'; - } elseif ($notification['type'] == Post\UserNotification::NOTIF_SHARED) { - $type = 'status'; - } elseif (in_array($notification['type'], [Post\UserNotification::NOTIF_EXPLICIT_TAGGED, - Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT, - Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT])) { - $type = 'mention'; - } else { + $type = ModelNotification::getType($notification); + if (empty($type)) { return null; } diff --git a/src/Model/Notification.php b/src/Model/Notification.php index cf2c754631..39fd6a9289 100644 --- a/src/Model/Notification.php +++ b/src/Model/Notification.php @@ -25,6 +25,7 @@ use Friendica\BaseModel; use Friendica\Content\Text\BBCode; use Friendica\Database\Database; use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Protocol\Activity; use Psr\Log\LoggerInterface; /** @@ -123,4 +124,34 @@ class Notification extends BaseModel return $message; } + + /** + * Fetch the notification type for the given notification + * + * @param array $notification + * @return string + */ + public static function getType(array $notification): string + { + if (($notification['vid'] == Verb::getID(Activity::FOLLOW)) && ($notification['type'] == Post\UserNotification::NOTIF_NONE)) { + $contact = Contact::getById($notification['actor-id'], ['pending']); + $contact['pending'] ? $type = 'follow_request' : 'follow'; + } elseif (($notification['vid'] == Verb::getID(Activity::ANNOUNCE)) && + in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { + $type = 'reblog'; + } elseif (in_array($notification['vid'], [Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]) && + in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) { + $type = 'favourite'; + } elseif ($notification['type'] == Post\UserNotification::NOTIF_SHARED) { + $type = 'status'; + } elseif (in_array($notification['type'], [Post\UserNotification::NOTIF_EXPLICIT_TAGGED, + Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT, + Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT])) { + $type = 'mention'; + } else { + return ''; + } + + return $type; + } } diff --git a/src/Model/Post/UserNotification.php b/src/Model/Post/UserNotification.php index 2c942d2cb9..f4bcb3e402 100644 --- a/src/Model/Post/UserNotification.php +++ b/src/Model/Post/UserNotification.php @@ -30,6 +30,7 @@ use Friendica\Database\DBStructure; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Post; +use Friendica\Model\Subscription; use Friendica\Util\Strings; use Friendica\Model\Tag; use Friendica\Protocol\Activity; @@ -297,7 +298,14 @@ class UserNotification $fields['target-uri-id'] = $item['uri-id']; } - return DBA::insert('notification', $fields, Database::INSERT_IGNORE); + $ret = DBA::insert('notification', $fields, Database::INSERT_IGNORE); + if ($ret) { + $id = DBA::lastInsertId(); + if (!empty($id)) { + Subscription::pushByNotificationId($id); + } + } + return $ret; } /** @@ -318,7 +326,14 @@ class UserNotification 'created' => DateTimeFormat::utcNow(), ]; - return DBA::insert('notification', $fields, Database::INSERT_IGNORE); + $ret = DBA::insert('notification', $fields, Database::INSERT_IGNORE); + if ($ret) { + $id = DBA::lastInsertId(); + if (!empty($id)) { + Subscription::pushByNotificationId($id); + } + } + return $ret; } /** diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index fa93eb5218..56b4625451 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -23,8 +23,10 @@ * @see https://github.com/web-push-libs/web-push-php * Possibly we should simply use this. */ + namespace Friendica\Model; +use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Util\Crypto; @@ -110,4 +112,25 @@ class Subscription } return $keypair['vapid-public']; } + + /** + * Prepare push notification + * + * @param int $nid + * @return void + */ + public static function pushByNotificationId(int $nid) + { + $notification = DBA::selectFirst('notification', [], ['id' => $nid]); + $type = Notification::getType($notification); + if (empty($type)) { + return; + } + + $subscriptions = DBA::select('subscription', [], ['uid' => $notification['uid'], $type => true]); + while ($subscription = DBA::fetch($subscriptions)) { + Logger::info('Push notification', ['id' => $subscription['id'], 'uid' => $subscription['uid'], 'type' => $type]); + } + DBA::close($subscriptions); + } } From f29cef175ed4e6549989055a8476aea8556c1f34 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 16:22:23 +0000 Subject: [PATCH 8/9] Remove spaces --- src/Model/Subscription.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index 56b4625451..7401eb9273 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -116,8 +116,8 @@ class Subscription /** * Prepare push notification * - * @param int $nid - * @return void + * @param int $nid + * @return void */ public static function pushByNotificationId(int $nid) { From c78d490c2e42aa996adc821bdad4e8e733e29184 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 15 Aug 2021 16:24:12 +0000 Subject: [PATCH 9/9] Some more standards --- src/Model/Subscription.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index 7401eb9273..aaaa7db7af 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -122,6 +122,7 @@ class Subscription public static function pushByNotificationId(int $nid) { $notification = DBA::selectFirst('notification', [], ['id' => $nid]); + $type = Notification::getType($notification); if (empty($type)) { return;