Removed Twitter integrations.
This commit is contained in:
parent
521a8fb0d1
commit
d05046ff1f
17 changed files with 2 additions and 638 deletions
|
@ -6,9 +6,7 @@ If you need to reach us outside of this website, this is the page for you. Below
|
||||||
- [flash](mailto:flashii@flash.moe): Site Administrator
|
- [flash](mailto:flashii@flash.moe): Site Administrator
|
||||||
|
|
||||||
## Twitter
|
## Twitter
|
||||||
- [@flashiinet](https://twitter.com/flashiinet): General updates and conversation.
|
|
||||||
- [@flashiistatus](https://twitter.com/flashiistatus): Exclusively system status updates, posts by this accounts are generally retweeted by @flashiinet.
|
- [@flashiistatus](https://twitter.com/flashiistatus): Exclusively system status updates, posts by this accounts are generally retweeted by @flashiinet.
|
||||||
- [@flashwahaha](https://twitter.com/flashwahaha): Twitter of the owner. This is a personal space, proceed with caution!
|
|
||||||
|
|
||||||
## Source Code
|
## Source Code
|
||||||
- [Misuzu](https://git.flash.moe/flashii/misuzu): Backend of the main website.
|
- [Misuzu](https://git.flash.moe/flashii/misuzu): Backend of the main website.
|
||||||
|
|
|
@ -152,7 +152,6 @@ Template::set('globals', [
|
||||||
'site_name' => $cfg->getValue('site.name', IConfig::T_STR, 'Misuzu'),
|
'site_name' => $cfg->getValue('site.name', IConfig::T_STR, 'Misuzu'),
|
||||||
'site_description' => $cfg->getValue('site.desc', IConfig::T_STR),
|
'site_description' => $cfg->getValue('site.desc', IConfig::T_STR),
|
||||||
'site_url' => $cfg->getValue('site.url', IConfig::T_STR),
|
'site_url' => $cfg->getValue('site.url', IConfig::T_STR),
|
||||||
'site_twitter' => $cfg->getValue('social.twitter', IConfig::T_STR),
|
|
||||||
'site_chat' => $cfg->getValue('sockChat.chatPath.normal', IConfig::T_STR),
|
'site_chat' => $cfg->getValue('sockChat.chatPath.normal', IConfig::T_STR),
|
||||||
'eeprom' => [
|
'eeprom' => [
|
||||||
'path' => $cfg->getValue('eeprom.path', IConfig::T_STR),
|
'path' => $cfg->getValue('eeprom.path', IConfig::T_STR),
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu;
|
|
||||||
|
|
||||||
use Misuzu\Users\User;
|
|
||||||
use Misuzu\Twitter\TwitterAccessToken;
|
|
||||||
use Misuzu\Twitter\TwitterClient;
|
|
||||||
|
|
||||||
require_once '../../../misuzu.php';
|
|
||||||
|
|
||||||
if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_TWITTER)) {
|
|
||||||
echo render_error(403);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tCfg = $cfg->scopeTo('twitter');
|
|
||||||
|
|
||||||
$tClient = $msz->createTwitterClient();
|
|
||||||
$tHasClientId = $tClient->hasClientId();
|
|
||||||
$tHasAccessToken = $tClient->hasAccessToken();
|
|
||||||
$tHasRefreshToken = $tClient->hasRefreshToken();
|
|
||||||
$tExpires = $tClient->getAccessToken()->getExpiresTime();
|
|
||||||
|
|
||||||
if(isset($_GET['m'])) {
|
|
||||||
if(CSRF::validateRequest()) {
|
|
||||||
$mode = (string)filter_input(INPUT_GET, 'm');
|
|
||||||
|
|
||||||
if($mode === 'authorise' && $tHasClientId && !$tHasAccessToken) {
|
|
||||||
$tAuthorise = $tClient->authorise(TwitterClient::SYSTEM_SCOPES, url_prefix(false) . url('twitter-callback'));
|
|
||||||
setcookie('msz_twitter', $tAuthorise->getVerifier(), strtotime('+5 minutes'), '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
|
||||||
header('Location: ' . $tAuthorise->getUri());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($mode === 'refresh' && $tHasClientId && $tHasAccessToken && $tHasRefreshToken) {
|
|
||||||
$tRefresh = TwitterAccessToken::fromTwitterResponse($tClient->authRefresh());
|
|
||||||
TwitterAccessToken::save($tCfg->scopeTo('access'), $tRefresh);
|
|
||||||
header('Location: ' . url('manage-general-twitter'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($mode === 'revoke' && $tHasClientId && $tHasAccessToken) {
|
|
||||||
$tRevoke = $tClient->authRevoke();
|
|
||||||
if(!empty($tRevoke->revoked))
|
|
||||||
TwitterAccessToken::nuke($tCfg->scopeTo('access'));
|
|
||||||
|
|
||||||
header('Location: ' . url('manage-general-twitter'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Location: ' . url('manage-general-twitter'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Template::render('manage.general.twitter', [
|
|
||||||
'twitter_has_oauth2' => $tHasClientId,
|
|
||||||
'twitter_has_access' => $tHasAccessToken,
|
|
||||||
'twitter_has_refresh' => $tHasRefreshToken,
|
|
||||||
'twitter_expires' => $tExpires,
|
|
||||||
]);
|
|
|
@ -53,12 +53,7 @@ if(!empty($_POST['post']) && CSRF::validateRequest()) {
|
||||||
|
|
||||||
if(!empty($isNew)) {
|
if(!empty($isNew)) {
|
||||||
if($postInfo->isFeatured()) {
|
if($postInfo->isFeatured()) {
|
||||||
$twitter = $msz->createTwitterClient();
|
// Twitter integration used to be here, replace with Railgun Pulse integration
|
||||||
|
|
||||||
if($twitter->hasAccessToken()) {
|
|
||||||
$url = url('news-post', ['post' => $postInfo->getId()]);
|
|
||||||
$twitter->sendTweet("News :: {$postInfo->getTitle()}\nhttps://{$_SERVER['HTTP_HOST']}{$url}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()]));
|
header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()]));
|
||||||
|
|
|
@ -5,7 +5,6 @@ use Misuzu\DB;
|
||||||
use Misuzu\MisuzuContext;
|
use Misuzu\MisuzuContext;
|
||||||
use Misuzu\Console\CommandArgs;
|
use Misuzu\Console\CommandArgs;
|
||||||
use Misuzu\Console\CommandInterface;
|
use Misuzu\Console\CommandInterface;
|
||||||
use Misuzu\Twitter\TwitterAccessToken;
|
|
||||||
|
|
||||||
class CronCommand implements CommandInterface {
|
class CronCommand implements CommandInterface {
|
||||||
private MisuzuContext $context;
|
private MisuzuContext $context;
|
||||||
|
@ -45,14 +44,6 @@ class CronCommand implements CommandInterface {
|
||||||
forum_count_synchronise();
|
forum_count_synchronise();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function refreshTwitterToken(): void {
|
|
||||||
$tClient = $this->context->createTwitterClient();
|
|
||||||
if($tClient->hasAccessToken() && $tClient->hasRefreshToken()) {
|
|
||||||
$tRefresh = TwitterAccessToken::fromTwitterResponse($tClient->authRefresh());
|
|
||||||
TwitterAccessToken::save($this->context->getConfig()->scopeTo('twitter.access'), $tRefresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const TASKS = [
|
private const TASKS = [
|
||||||
[
|
[
|
||||||
'name' => 'Ensures main role exists.',
|
'name' => 'Ensures main role exists.',
|
||||||
|
@ -171,11 +162,5 @@ class CronCommand implements CommandInterface {
|
||||||
WHERE `tfa_created` < NOW() - INTERVAL 15 MINUTE
|
WHERE `tfa_created` < NOW() - INTERVAL 15 MINUTE
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'name' => 'Refresh Twitter authentication token.',
|
|
||||||
'type' => 'func',
|
|
||||||
'slow' => true,
|
|
||||||
'command' => 'refreshTwitterToken',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ use Misuzu\Config\IConfig;
|
||||||
use Misuzu\GeoIP\GeoIPHelper;
|
use Misuzu\GeoIP\GeoIPHelper;
|
||||||
use Misuzu\SharpChat\SharpChatRoutes;
|
use Misuzu\SharpChat\SharpChatRoutes;
|
||||||
use Misuzu\Users\Users;
|
use Misuzu\Users\Users;
|
||||||
use Misuzu\Twitter\TwitterClient;
|
|
||||||
use Misuzu\Twitter\TwitterRoutes;
|
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
use Index\Data\Migration\IDbMigrationRepo;
|
use Index\Data\Migration\IDbMigrationRepo;
|
||||||
use Index\Data\Migration\DbMigrationManager;
|
use Index\Data\Migration\DbMigrationManager;
|
||||||
|
@ -63,10 +61,6 @@ class MisuzuContext {
|
||||||
return $this->geoIP;
|
return $this->geoIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createTwitterClient(): TwitterClient {
|
|
||||||
return TwitterClient::create($this->config->scopeTo('twitter'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUpHttp(bool $legacy = false): void {
|
public function setUpHttp(bool $legacy = false): void {
|
||||||
$this->router = new HttpFx;
|
$this->router = new HttpFx;
|
||||||
$this->router->use('/', function($response) {
|
$this->router->use('/', function($response) {
|
||||||
|
@ -136,7 +130,6 @@ class MisuzuContext {
|
||||||
$this->router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST'));
|
$this->router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST'));
|
||||||
|
|
||||||
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'));
|
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'));
|
||||||
new TwitterRoutes($this, $this->router, $this->config->scopeTo('twitter'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function registerLegacyRedirects(): void {
|
private function registerLegacyRedirects(): void {
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu\Twitter;
|
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
use Misuzu\Config\IConfig;
|
|
||||||
|
|
||||||
class TwitterAccessToken implements Stringable {
|
|
||||||
public function __construct(
|
|
||||||
private string $type,
|
|
||||||
private string $accessToken,
|
|
||||||
private int $expires,
|
|
||||||
private array $scope,
|
|
||||||
private string $refreshToken
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function getType(): string {
|
|
||||||
return $this->type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAccessToken(): string {
|
|
||||||
return $this->accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasAccessToken(): bool {
|
|
||||||
return $this->type !== '' && $this->accessToken !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExpiresTime(): int {
|
|
||||||
return $this->expires;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasExpires(): bool {
|
|
||||||
return time() > $this->expires;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getScope(): array {
|
|
||||||
return $this->scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRefreshToken(): string {
|
|
||||||
return $this->refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasRefreshToken(): bool {
|
|
||||||
return $this->refreshToken !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string {
|
|
||||||
return 'Bearer ' . $this->accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function empty(): self {
|
|
||||||
return new static('', '', 0, [], '');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromTwitterResponse(object $obj): self {
|
|
||||||
return new static(
|
|
||||||
$obj->token_type ?? '',
|
|
||||||
$obj->access_token ?? '',
|
|
||||||
time() + ($obj->expires_in ?? 0),
|
|
||||||
explode(' ', ($obj->scope ?? '')),
|
|
||||||
$obj->refresh_token ?? ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function load(IConfig $config): self {
|
|
||||||
return new static(
|
|
||||||
$config->getValue('type', IConfig::T_STR),
|
|
||||||
$config->getValue('token', IConfig::T_STR),
|
|
||||||
$config->getValue('expires', IConfig::T_INT),
|
|
||||||
$config->getValue('token', IConfig::T_ARR),
|
|
||||||
$config->getValue('refresh', IConfig::T_STR)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function save(IConfig $config, self $tokenInfo): void {
|
|
||||||
$config->setValue('type', $tokenInfo->getType());
|
|
||||||
$config->setValue('token', $tokenInfo->getAccessToken());
|
|
||||||
$config->setValue('expires', $tokenInfo->getExpiresTime());
|
|
||||||
$config->setValue('scope', $tokenInfo->getScope());
|
|
||||||
$config->setValue('refresh', $tokenInfo->getRefreshToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function nuke(IConfig $config): void {
|
|
||||||
$config->removeValue('type');
|
|
||||||
$config->removeValue('token');
|
|
||||||
$config->removeValue('expires');
|
|
||||||
$config->removeValue('scope');
|
|
||||||
$config->removeValue('refresh');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu\Twitter;
|
|
||||||
|
|
||||||
use Index\XString;
|
|
||||||
use Index\Serialisation\Serialiser;
|
|
||||||
|
|
||||||
class TwitterAuthorisation {
|
|
||||||
private const AUTHORIZE = 'https://twitter.com/i/oauth2/authorize';
|
|
||||||
|
|
||||||
private const STATE_RNG_LENGTH = 16;
|
|
||||||
private const STATE_EPOCH = 1661126400;
|
|
||||||
private const STATE_TOLERANCE = 5 * 60;
|
|
||||||
private const VERIFIER_LENGTH = 48;
|
|
||||||
|
|
||||||
private TwitterClientId $clientId;
|
|
||||||
private array $scope;
|
|
||||||
private string $redirect;
|
|
||||||
private string $state;
|
|
||||||
private string $verifier;
|
|
||||||
private string $verifierHash;
|
|
||||||
|
|
||||||
public function __construct(TwitterClientId $clientId, array $scope, string $redirect) {
|
|
||||||
$this->clientId = $clientId;
|
|
||||||
$this->scope = $scope;
|
|
||||||
$this->redirect = $redirect;
|
|
||||||
|
|
||||||
$this->state = self::generateState($clientId);
|
|
||||||
[$this->verifier, $this->verifierHash] = self::generateVerifier();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClientId(): TwitterClientId {
|
|
||||||
return $this->clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getScope(): array {
|
|
||||||
return $this->scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRedirectUri(): string {
|
|
||||||
return $this->redirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getState(): string {
|
|
||||||
return $this->state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getVerifier(): string {
|
|
||||||
return $this->verifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getVerifierHash(): string {
|
|
||||||
return $this->verifierHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUri(): string {
|
|
||||||
return self::AUTHORIZE . '?' . http_build_query([
|
|
||||||
'response_type' => 'code',
|
|
||||||
'client_id' => $this->clientId->getClientId(),
|
|
||||||
'redirect_uri' => $this->redirect,
|
|
||||||
'scope' => implode(' ', $this->scope),
|
|
||||||
'state' => $this->state,
|
|
||||||
'code_challenge' => $this->verifierHash,
|
|
||||||
'code_challenge_method' => 'S256',
|
|
||||||
], '', null, PHP_QUERY_RFC3986);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function generateVerifier(): array {
|
|
||||||
$verifier = XString::random(self::VERIFIER_LENGTH);
|
|
||||||
return [
|
|
||||||
$verifier,
|
|
||||||
Serialiser::uriBase64()->serialise(hash('sha256', $verifier, true)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function currentStateTime(): int {
|
|
||||||
return time() - self::STATE_EPOCH;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function generateState(TwitterClientId $clientId): string {
|
|
||||||
$rng = XString::random(self::STATE_RNG_LENGTH);
|
|
||||||
$time = self::currentStateTime();
|
|
||||||
|
|
||||||
$string = $rng . ':' . (string)$time;
|
|
||||||
$hash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);
|
|
||||||
|
|
||||||
$time = Serialiser::base62()->serialise($time);
|
|
||||||
$hash = Serialiser::uriBase64()->serialise($hash);
|
|
||||||
|
|
||||||
return $rng . '.' . $time . '.' . $hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function verifyState(TwitterClientId $clientId, string $state): bool {
|
|
||||||
$parts = explode('.', $state, 4);
|
|
||||||
if(count($parts) !== 3)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$rng = $parts[0];
|
|
||||||
if(strlen($rng) !== self::STATE_RNG_LENGTH)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$currentTime = self::currentStateTime();
|
|
||||||
$time = Serialiser::base62()->deserialise($parts[1]);
|
|
||||||
if($currentTime < $time || $currentTime >= ($time + self::STATE_TOLERANCE))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$hash = Serialiser::uriBase64()->deserialise($parts[2]);
|
|
||||||
if(strlen($hash) !== 32)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$string = $rng . ':' . (string)$time;
|
|
||||||
$realHash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);
|
|
||||||
|
|
||||||
return hash_equals($realHash, $hash);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
<?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'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu\Twitter;
|
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
use Misuzu\Config\IConfig;
|
|
||||||
|
|
||||||
class TwitterClientId implements Stringable {
|
|
||||||
public function __construct(
|
|
||||||
private string $clientId,
|
|
||||||
private string $clientSecret
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function hasClientId(): bool {
|
|
||||||
return $this->clientId !== '' && $this->clientSecret !== '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClientId(): string {
|
|
||||||
return $this->clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClientSecret(): string {
|
|
||||||
return $this->clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string {
|
|
||||||
return 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function load(IConfig $config): self {
|
|
||||||
return new static(
|
|
||||||
$config->getValue('clientId', IConfig::T_STR),
|
|
||||||
$config->getValue('clientSecret', IConfig::T_STR)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu\Twitter;
|
|
||||||
|
|
||||||
use Index\Routing\IRouter;
|
|
||||||
use Misuzu\MisuzuContext;
|
|
||||||
use Misuzu\Config\IConfig;
|
|
||||||
use Misuzu\Twitter\TwitterAccessToken;
|
|
||||||
use Misuzu\Twitter\TwitterAuthorisation;
|
|
||||||
use Misuzu\Twitter\TwitterClient;
|
|
||||||
use Misuzu\Twitter\TwitterClientId;
|
|
||||||
|
|
||||||
final class TwitterRoutes {
|
|
||||||
private MisuzuContext $context;
|
|
||||||
private IConfig $config;
|
|
||||||
private ?TwitterClientId $clientId = null;
|
|
||||||
|
|
||||||
public function __construct(MisuzuContext $ctx, IRouter $router, IConfig $config) {
|
|
||||||
$this->context = $ctx;
|
|
||||||
$this->config = $config;
|
|
||||||
|
|
||||||
$router->get('/_twitter/callback', [$this, 'callback']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getClientId(): TwitterClientId {
|
|
||||||
if($this->clientId === null)
|
|
||||||
$this->clientId = TwitterClientId::load($this->config->scopeTo('oauth2'));
|
|
||||||
return $this->clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function callback($response, $request) {
|
|
||||||
$qState = (string)$request->getParam('state');
|
|
||||||
$qCode = (string)$request->getParam('code');
|
|
||||||
$cVerifier = (string)$request->getCookie('msz_twitter');
|
|
||||||
|
|
||||||
if(empty($qState) || empty($qCode) || empty($cVerifier))
|
|
||||||
return 400;
|
|
||||||
|
|
||||||
$response->removeCookie('msz_twitter', '/', msz_cookie_domain(), !empty($_SERVER['HTTPS']), true);
|
|
||||||
|
|
||||||
$clientId = $this->getClientId();
|
|
||||||
if(!TwitterAuthorisation::verifyState($clientId, $qState))
|
|
||||||
return 403;
|
|
||||||
|
|
||||||
$accessToken = TwitterAccessToken::empty();
|
|
||||||
$client = new TwitterClient($clientId, $accessToken);
|
|
||||||
|
|
||||||
$redirect = url_prefix(false) . url('twitter-callback');
|
|
||||||
$tokenInfo = TwitterAccessToken::fromTwitterResponse($client->token($qCode, $cVerifier, $redirect));
|
|
||||||
TwitterAccessToken::save($this->config->scopeTo('access'), $tokenInfo);
|
|
||||||
|
|
||||||
$response->redirect(url('manage-general-twitter'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,8 +15,6 @@ function manage_get_menu(int $userId): array {
|
||||||
$menu['General']['Emoticons'] = url('manage-general-emoticons');
|
$menu['General']['Emoticons'] = url('manage-general-emoticons');
|
||||||
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_CONFIG))
|
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_CONFIG))
|
||||||
$menu['General']['Settings'] = url('manage-general-settings');
|
$menu['General']['Settings'] = url('manage-general-settings');
|
||||||
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_TWITTER))
|
|
||||||
$menu['General']['Twitter Connection'] = url('manage-general-twitter');
|
|
||||||
|
|
||||||
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_USERS))
|
if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_USERS))
|
||||||
$menu['Users & Roles']['Users'] = url('manage-users');
|
$menu['Users & Roles']['Users'] = url('manage-users');
|
||||||
|
@ -135,11 +133,6 @@ function manage_perms_list(array $rawPerms): array {
|
||||||
'title' => 'Can manage general Misuzu settings.',
|
'title' => 'Can manage general Misuzu settings.',
|
||||||
'perm' => MSZ_PERM_GENERAL_MANAGE_CONFIG,
|
'perm' => MSZ_PERM_GENERAL_MANAGE_CONFIG,
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'section' => 'manage-twitter',
|
|
||||||
'title' => 'Can manage Twitter connection.',
|
|
||||||
'perm' => MSZ_PERM_GENERAL_MANAGE_TWITTER,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -6,7 +6,7 @@ define('MSZ_PERM_GENERAL_MANAGE_EMOTES', 0x00000004);
|
||||||
define('MSZ_PERM_GENERAL_MANAGE_CONFIG', 0x00000008);
|
define('MSZ_PERM_GENERAL_MANAGE_CONFIG', 0x00000008);
|
||||||
//define('MSZ_PERM_GENERAL_IS_TESTER', 0x00000010); Has been unused for a while
|
//define('MSZ_PERM_GENERAL_IS_TESTER', 0x00000010); Has been unused for a while
|
||||||
//define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020); Blacklist has been removed for now to reduce overhead and because it was broken(?)
|
//define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020); Blacklist has been removed for now to reduce overhead and because it was broken(?)
|
||||||
define('MSZ_PERM_GENERAL_MANAGE_TWITTER', 0x00000040);
|
//define('MSZ_PERM_GENERAL_MANAGE_TWITTER', 0x00000040); Twitter integration has been removed
|
||||||
|
|
||||||
define('MSZ_PERMS_USER', 'user');
|
define('MSZ_PERMS_USER', 'user');
|
||||||
define('MSZ_PERM_USER_EDIT_PROFILE', 0x00000001);
|
define('MSZ_PERM_USER_EDIT_PROFILE', 0x00000001);
|
||||||
|
|
|
@ -85,13 +85,10 @@ define('MSZ_URLS', [
|
||||||
'comment-pin' => ['/comments.php', ['c' => '<comment>', 'csrf' => '{csrf}', 'm' => 'pin']],
|
'comment-pin' => ['/comments.php', ['c' => '<comment>', 'csrf' => '{csrf}', 'm' => 'pin']],
|
||||||
'comment-unpin' => ['/comments.php', ['c' => '<comment>', 'csrf' => '{csrf}', 'm' => 'unpin']],
|
'comment-unpin' => ['/comments.php', ['c' => '<comment>', 'csrf' => '{csrf}', 'm' => 'unpin']],
|
||||||
|
|
||||||
'twitter-callback' => ['/_twitter/callback'],
|
|
||||||
|
|
||||||
'manage-index' => ['/manage'],
|
'manage-index' => ['/manage'],
|
||||||
|
|
||||||
'manage-general-overview' => ['/manage/general'],
|
'manage-general-overview' => ['/manage/general'],
|
||||||
'manage-general-logs' => ['/manage/general/logs.php'],
|
'manage-general-logs' => ['/manage/general/logs.php'],
|
||||||
'manage-general-twitter' => ['/manage/general/twitter.php'],
|
|
||||||
|
|
||||||
'manage-general-emoticons' => ['/manage/general/emoticons.php'],
|
'manage-general-emoticons' => ['/manage/general/emoticons.php'],
|
||||||
'manage-general-emoticon' => ['/manage/general/emoticon.php', ['e' => '<emote>']],
|
'manage-general-emoticon' => ['/manage/general/emoticon.php', ['e' => '<emote>']],
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% apply spaceless %}
|
{% apply spaceless %}
|
||||||
{% set description = description|default(globals.site_description) %}
|
{% set description = description|default(globals.site_description) %}
|
||||||
{% set site_twitter = site_twitter|default(globals.site_twitter) %}
|
|
||||||
|
|
||||||
{% if title is defined %}
|
{% if title is defined %}
|
||||||
{% set browser_title = title ~ ' :: ' ~ globals.site_name %}
|
{% set browser_title = title ~ ' :: ' ~ globals.site_name %}
|
||||||
|
@ -10,22 +9,15 @@
|
||||||
|
|
||||||
<title>{{ browser_title }}</title>
|
<title>{{ browser_title }}</title>
|
||||||
|
|
||||||
<meta name="twitter:title" content="{{ title|default(globals.site_name)|slice(0, 70) }}">
|
|
||||||
<meta property="og:title" content="{{ title|default(globals.site_name) }}">
|
<meta property="og:title" content="{{ title|default(globals.site_name) }}">
|
||||||
<meta property="og:site_name" content="{{ globals.site_name }}">
|
<meta property="og:site_name" content="{{ globals.site_name }}">
|
||||||
|
|
||||||
{% if description|length > 0 %}
|
{% if description|length > 0 %}
|
||||||
<meta name="description" content="{{ description }}">
|
<meta name="description" content="{{ description }}">
|
||||||
<meta name="twitter:description" content="{{ description }}">
|
|
||||||
<meta property="og:description" content="{{ description }}">
|
<meta property="og:description" content="{{ description }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if site_twitter|length > 0 %}
|
|
||||||
<meta name="twitter:site" content="{{ site_twitter }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<meta property="og:type" content="object">
|
<meta property="og:type" content="object">
|
||||||
<meta name="twitter:card" content="summary">
|
|
||||||
|
|
||||||
{% if image is defined %}
|
{% if image is defined %}
|
||||||
{% if image|slice(0, 1) == '/' %}
|
{% if image|slice(0, 1) == '/' %}
|
||||||
|
@ -37,7 +29,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if image|length > 0 %}
|
{% if image|length > 0 %}
|
||||||
<meta name="twitter:image:src" content="{{ image }}">
|
|
||||||
<meta property="og:image" content="{{ image }}">
|
<meta property="og:image" content="{{ image }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
{% extends 'manage/general/master.twig' %}
|
|
||||||
{% from 'macros.twig' import container_title %}
|
|
||||||
|
|
||||||
{% block manage_content %}
|
|
||||||
<div class="container manage-settings">
|
|
||||||
{{ container_title('<i class="fab fa-twitter fa-fw"></i> Twitter Connection') }}
|
|
||||||
|
|
||||||
<div class="manage__description">
|
|
||||||
Manages the Twitter connection for announcing news posts on the Twitter account.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if twitter_has_oauth2 %}
|
|
||||||
{% if twitter_has_access %}
|
|
||||||
<div style="padding: 2px 5px">
|
|
||||||
A Twitter user has been authenticated.
|
|
||||||
Current access token expires <time datetime="{{ twitter_expires|date('c') }}" title="{{ twitter_expires|date('r') }}">{{ twitter_expires|time_diff }}</time>.
|
|
||||||
</div>
|
|
||||||
<div class="manage__emote__actions">
|
|
||||||
{% if twitter_has_refresh %}
|
|
||||||
<a class="input__button" href="?m=refresh&csrf={{ csrf_token() }}">Refresh Access</a>
|
|
||||||
{% endif %}
|
|
||||||
<a class="input__button" href="?m=revoke&csrf={{ csrf_token() }}">Revoke Access</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div style="padding: 2px 5px">
|
|
||||||
No Twitter user has been authorised yet.
|
|
||||||
Before beginning authorization, make sure you're logged into Twitter with the desired user.
|
|
||||||
</div>
|
|
||||||
<div class="manage__emote__actions">
|
|
||||||
<a class="input__button" href="?m=authorise&csrf={{ csrf_token() }}">Begin Authorisation</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div style="padding: 2px 5px">
|
|
||||||
Twitter OAuth2 credentials have not been registered.
|
|
||||||
Add them through <a href="{{ url('manage-general-settings') }}" class="link">Settings</a> as <a href="{{ url('manage-general-setting', {'name': 'twitter.oauth2.clientId', 'type': 'string'}) }}" class="link"><code>twitter.oauth2.clientId</code></a> and <a href="{{ url('manage-general-setting', {'name': 'twitter.oauth2.clientSecret', 'type': 'string'}) }}" class="link"><code>twitter.oauth2.clientSecret</code></a>.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -48,11 +48,6 @@
|
||||||
'title': 'Rules',
|
'title': 'Rules',
|
||||||
'url': url('info', {'title': 'rules'}),
|
'url': url('info', {'title': 'rules'}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'title': 'Twitter',
|
|
||||||
'url': 'https://twitter.com/' ~ globals.site_twitter|default(''),
|
|
||||||
'display': globals.site_twitter is defined and globals.site_twitter is not empty,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue