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}