friendship ended with social-app. php is my new best friend
1<?php 2/** 3 * Class Mastodon 4 * 5 * @created 19.08.2018 6 * @author smiley <smiley@chillerlan.net> 7 * @copyright 2018 smiley 8 * @license MIT 9 * 10 * @noinspection PhpUnused 11 */ 12declare(strict_types=1); 13 14namespace chillerlan\OAuth\Providers; 15 16use chillerlan\OAuth\Core\{AccessToken, AuthenticatedUser, CSRFToken, OAuth2Provider, TokenRefresh, UserInfo}; 17use chillerlan\OAuth\OAuthException; 18use Psr\Http\Message\UriInterface; 19use function array_merge; 20 21/** 22 * Mastodon OAuth2 (v4.x instances) 23 * 24 * @link https://docs.joinmastodon.org/client/intro/ 25 * @link https://docs.joinmastodon.org/methods/apps/oauth/ 26 */ 27class Mastodon extends OAuth2Provider implements CSRFToken, TokenRefresh, UserInfo{ 28 29 public const IDENTIFIER = 'MASTODON'; 30 31 public const SCOPE_READ = 'read'; 32 public const SCOPE_WRITE = 'write'; 33 public const SCOPE_FOLLOW = 'follow'; 34 public const SCOPE_PUSH = 'push'; 35 36 public const DEFAULT_SCOPES = [ 37 self::SCOPE_READ, 38 self::SCOPE_FOLLOW, 39 ]; 40 41 protected string $authorizationURL = 'https://mastodon.social/oauth/authorize'; 42 protected string $accessTokenURL = 'https://mastodon.social/oauth/token'; 43 protected string $apiURL = 'https://mastodon.social/api'; 44 protected string|null $userRevokeURL = 'https://mastodon.social/oauth/authorized_applications'; 45 protected string|null $apiDocs = 'https://docs.joinmastodon.org/api/'; 46 protected string|null $applicationURL = 'https://mastodon.social/settings/applications'; 47 protected string $instance = 'https://mastodon.social'; 48 49 /** 50 * set the internal URLs for the given Mastodon instance 51 * 52 * @throws \chillerlan\OAuth\OAuthException 53 */ 54 public function setInstance(UriInterface|string $instance):static{ 55 56 if(!$instance instanceof UriInterface){ 57 $instance = $this->uriFactory->createUri($instance); 58 } 59 60 if($instance->getHost() === ''){ 61 throw new OAuthException('invalid instance URL'); 62 } 63 64 // enforce https and remove unnecessary parts 65 $instance = $instance->withScheme('https')->withQuery('')->withFragment(''); 66 67 // @todo: check if host exists/responds? 68 $this->instance = (string)$instance->withPath(''); 69 $this->apiURL = (string)$instance->withPath('/api'); 70 $this->authorizationURL = (string)$instance->withPath('/oauth/authorize'); 71 $this->accessTokenURL = (string)$instance->withPath('/oauth/token'); 72 $this->userRevokeURL = (string)$instance->withPath('/oauth/authorized_applications'); 73 $this->applicationURL = (string)$instance->withPath('/settings/applications'); 74 75 return $this; 76 } 77 78 public function getAccessToken(string $code, string|null $state = null):AccessToken{ 79 $this->checkState($state); // we're an instance of CSRFToken 80 81 $body = $this->getAccessTokenRequestBodyParams($code); 82 $response = $this->sendAccessTokenRequest($this->accessTokenURL, $body); 83 $token = $this->parseTokenResponse($response); 84 85 // store the instance the token belongs to 86 $token->extraParams = array_merge($token->extraParams, ['instance' => $this->instance]); 87 88 $this->storage->storeAccessToken($token, $this->name); 89 90 return $token; 91 } 92 93 /** @codeCoverageIgnore */ 94 public function me():AuthenticatedUser{ 95 $json = $this->getMeResponseData('/v1/accounts/verify_credentials'); 96 97 $userdata = [ 98 'data' => $json, 99 'avatar' => $json['avatar'], 100 'handle' => $json['acct'], 101 'displayName' => $json['display_name'], 102 'id' => $json['id'], 103 'url' => $json['url'], 104 ]; 105 106 return new AuthenticatedUser($userdata); 107 } 108 109}