misuzu/src/Twitter/TwitterClient.php

189 lines
6.3 KiB
PHP

<?php
namespace Misuzu\Twitter;
use RuntimeException;
use Misuzu\Config\IConfig;
class TwitterClient {
public const SYSTEM_SCOPES = [
'tweet.read', 'tweet.write',
'users.read', 'offline.access',
'follows.read', 'follows.write',
'like.read', 'like.write',
];
private const API_BASE = 'https://api.twitter.com';
private const API_V2 = self::API_BASE . '/2';
private const API_OAUTH2 = self::API_V2 . '/oauth2';
private const API_OAUTH2_TOKEN = self::API_OAUTH2 . '/token';
private const API_OAUTH2_REVOKE = self::API_OAUTH2 . '/revoke';
private const API_TWEETS = self::API_V2 . '/tweets';
public function __construct(
private TwitterClientId $clientId,
private TwitterAccessToken $accessToken
) {}
public function getClientId(): TwitterClientId {
return $this->clientId;
}
public function hasClientId(): bool {
return $this->clientId->hasClientId();
}
public function getAccessToken(): TwitterAccessToken {
return $this->accessToken;
}
public function hasAccessToken(): bool {
return $this->accessToken->hasAccessToken();
}
public function hasRefreshToken(): bool {
return $this->accessToken->hasRefreshToken();
}
public function authorise(array $scope, string $redirect): TwitterAuthorisation {
return new TwitterAuthorisation($this->clientId, $scope, $redirect);
}
public function token(string $code, string $verifier, string $redirect): object {
if(!$this->clientId->hasClientId())
throw new RuntimeException('Need OAuth2 info in order to manage tokens.');
$req = json_decode(self::request('POST', self::API_OAUTH2_TOKEN, [
'Authorization: ' . (string)$this->clientId,
], [], [
'code' => $code,
'grant_type' => 'authorization_code',
'code_verifier' => $verifier,
'redirect_uri' => $redirect, // needed because????????
], false));
if($req === false)
return new RuntimeException('Unable to parse token response.');
return $req;
}
public function authRefresh(): object {
if(!$this->clientId->hasClientId())
throw new RuntimeException('Need OAuth2 info in order to manage tokens.');
if(!$this->accessToken->hasRefreshToken())
throw new RuntimeException('There is no refresh token.');
$req = json_decode(self::request('POST', self::API_OAUTH2_TOKEN, [
'Authorization: ' . (string)$this->clientId,
], [], [
'refresh_token' => $this->accessToken->getRefreshToken(),
'grant_type' => 'refresh_token',
], false));
if($req === false)
return new RuntimeException('Unable to parse token response.');
return $req;
}
public function authRevoke(): object {
if(!$this->clientId->hasClientId())
throw new RuntimeException('Need OAuth2 info in order to manage tokens.');
if(!$this->accessToken->hasAccessToken())
throw new RuntimeException('Cannot revoke an access token we do not have.');
$req = json_decode(self::request('POST', self::API_OAUTH2_REVOKE, [
'Authorization: ' . (string)$this->clientId,
], [], [
'token' => $this->accessToken->getAccessToken(),
'token_type_hint' => 'access_token',
], false));
if($req === false)
return new RuntimeException('Unable to parse token response.');
return $req;
}
public function sendTweet(string $text): object {
if(!$this->accessToken->hasAccessToken())
throw new RuntimeException('Need access token in order to post Tweets.');
$req = json_decode(self::request('POST', self::API_TWEETS, [
'Authorization: ' . (string)$this->accessToken,
], [], [
'text' => $text,
]));
if($req === false)
return new RuntimeException('Unable to parse Tweet response.');
return $req;
}
public function request(
string $method,
string $uri,
array $headers = [],
array $queryFields = [],
mixed $bodyFields = [],
bool $bodyAsJson = true,
): string|bool {
if(!empty($queryFields))
$uri .= '?' . http_build_query($queryFields, '', null, PHP_QUERY_RFC3986);
$curl = curl_init($uri);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => true,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TCP_NODELAY => true,
CURLOPT_HEADER => false,
CURLOPT_NOBODY => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 2,
CURLOPT_USERAGENT => 'Misuzu TwitterClient/20230105',
]);
if($method === 'GET')
curl_setopt($curl, CURLOPT_HTTPGET, true);
elseif($method === 'HEAD') {
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_NOBODY, true);
} elseif($method === 'POST')
curl_setopt($curl, CURLOPT_POST, true);
else
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
if(!empty($bodyFields)) {
if($bodyAsJson) {
$headers[] = 'Content-Type: application/json';
$bodyFields = json_encode($bodyFields);
} elseif(is_array($bodyFields)) {
$headers[] = 'Content-Type: application/x-www-form-urlencoded';
$bodyFields = http_build_query($bodyFields, '', null, PHP_QUERY_RFC3986);
}
curl_setopt($curl, CURLOPT_POSTFIELDS, $bodyFields);
}
if(!empty($headers))
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$out = curl_exec($curl);
curl_close($curl);
return $out;
}
public static function create(IConfig $config): self {
return new static(
TwitterClientId::load($config->scopeTo('oauth2')),
TwitterAccessToken::load($config->scopeTo('access'))
);
}
}