Rewrote Twitter connection (v1.1 -> v2).
This commit is contained in:
parent
cb40fdc7c4
commit
eafdc28d5e
22 changed files with 647 additions and 344 deletions
|
@ -3,7 +3,6 @@
|
||||||
"twig/twig": "^3.0",
|
"twig/twig": "^3.0",
|
||||||
"erusev/parsedown": "~1.6",
|
"erusev/parsedown": "~1.6",
|
||||||
"geoip2/geoip2": "~2.0",
|
"geoip2/geoip2": "~2.0",
|
||||||
"jublonet/codebird-php": "^3.1",
|
|
||||||
"chillerlan/php-qrcode": "^4.3",
|
"chillerlan/php-qrcode": "^4.3",
|
||||||
"whichbrowser/parser": "^2.0",
|
"whichbrowser/parser": "^2.0",
|
||||||
"symfony/mailer": "^6.0"
|
"symfony/mailer": "^6.0"
|
||||||
|
|
219
composer.lock
generated
219
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d3c39d122a38515484c9d439ecee240b",
|
"content-hash": "b3b6cf189969ecb1dd838c23a4fe96e1",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "chillerlan/php-qrcode",
|
"name": "chillerlan/php-qrcode",
|
||||||
|
@ -223,157 +223,6 @@
|
||||||
],
|
],
|
||||||
"time": "2021-10-28T20:44:15+00:00"
|
"time": "2021-10-28T20:44:15+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "composer/installers",
|
|
||||||
"version": "v1.12.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/composer/installers.git",
|
|
||||||
"reference": "d20a64ed3c94748397ff5973488761b22f6d3f19"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19",
|
|
||||||
"reference": "d20a64ed3c94748397ff5973488761b22f6d3f19",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-plugin-api": "^1.0 || ^2.0"
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"roundcube/plugin-installer": "*",
|
|
||||||
"shama/baton": "*"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"composer/composer": "1.6.* || ^2.0",
|
|
||||||
"composer/semver": "^1 || ^3",
|
|
||||||
"phpstan/phpstan": "^0.12.55",
|
|
||||||
"phpstan/phpstan-phpunit": "^0.12.16",
|
|
||||||
"symfony/phpunit-bridge": "^4.2 || ^5",
|
|
||||||
"symfony/process": "^2.3"
|
|
||||||
},
|
|
||||||
"type": "composer-plugin",
|
|
||||||
"extra": {
|
|
||||||
"class": "Composer\\Installers\\Plugin",
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Composer\\Installers\\": "src/Composer/Installers"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Kyle Robinson Young",
|
|
||||||
"email": "kyle@dontkry.com",
|
|
||||||
"homepage": "https://github.com/shama"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A multi-framework Composer library installer",
|
|
||||||
"homepage": "https://composer.github.io/installers/",
|
|
||||||
"keywords": [
|
|
||||||
"Craft",
|
|
||||||
"Dolibarr",
|
|
||||||
"Eliasis",
|
|
||||||
"Hurad",
|
|
||||||
"ImageCMS",
|
|
||||||
"Kanboard",
|
|
||||||
"Lan Management System",
|
|
||||||
"MODX Evo",
|
|
||||||
"MantisBT",
|
|
||||||
"Mautic",
|
|
||||||
"Maya",
|
|
||||||
"OXID",
|
|
||||||
"Plentymarkets",
|
|
||||||
"Porto",
|
|
||||||
"RadPHP",
|
|
||||||
"SMF",
|
|
||||||
"Starbug",
|
|
||||||
"Thelia",
|
|
||||||
"Whmcs",
|
|
||||||
"WolfCMS",
|
|
||||||
"agl",
|
|
||||||
"aimeos",
|
|
||||||
"annotatecms",
|
|
||||||
"attogram",
|
|
||||||
"bitrix",
|
|
||||||
"cakephp",
|
|
||||||
"chef",
|
|
||||||
"cockpit",
|
|
||||||
"codeigniter",
|
|
||||||
"concrete5",
|
|
||||||
"croogo",
|
|
||||||
"dokuwiki",
|
|
||||||
"drupal",
|
|
||||||
"eZ Platform",
|
|
||||||
"elgg",
|
|
||||||
"expressionengine",
|
|
||||||
"fuelphp",
|
|
||||||
"grav",
|
|
||||||
"installer",
|
|
||||||
"itop",
|
|
||||||
"joomla",
|
|
||||||
"known",
|
|
||||||
"kohana",
|
|
||||||
"laravel",
|
|
||||||
"lavalite",
|
|
||||||
"lithium",
|
|
||||||
"magento",
|
|
||||||
"majima",
|
|
||||||
"mako",
|
|
||||||
"mediawiki",
|
|
||||||
"miaoxing",
|
|
||||||
"modulework",
|
|
||||||
"modx",
|
|
||||||
"moodle",
|
|
||||||
"osclass",
|
|
||||||
"pantheon",
|
|
||||||
"phpbb",
|
|
||||||
"piwik",
|
|
||||||
"ppi",
|
|
||||||
"processwire",
|
|
||||||
"puppet",
|
|
||||||
"pxcms",
|
|
||||||
"reindex",
|
|
||||||
"roundcube",
|
|
||||||
"shopware",
|
|
||||||
"silverstripe",
|
|
||||||
"sydes",
|
|
||||||
"sylius",
|
|
||||||
"symfony",
|
|
||||||
"tastyigniter",
|
|
||||||
"typo3",
|
|
||||||
"wordpress",
|
|
||||||
"yawik",
|
|
||||||
"zend",
|
|
||||||
"zikula"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/composer/installers/issues",
|
|
||||||
"source": "https://github.com/composer/installers/tree/v1.12.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://packagist.com",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/composer",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2021-09-13T08:19:44+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "doctrine/lexer",
|
"name": "doctrine/lexer",
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
|
@ -626,70 +475,6 @@
|
||||||
},
|
},
|
||||||
"time": "2021-11-30T18:15:25+00:00"
|
"time": "2021-11-30T18:15:25+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "jublonet/codebird-php",
|
|
||||||
"version": "3.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/jublo/codebird-php.git",
|
|
||||||
"reference": "100a8e8f1928a5738b4476f0caf83f2c2ba6da5b"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/jublo/codebird-php/zipball/100a8e8f1928a5738b4476f0caf83f2c2ba6da5b",
|
|
||||||
"reference": "100a8e8f1928a5738b4476f0caf83f2c2ba6da5b",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer/installers": "~1.0",
|
|
||||||
"ext-hash": "*",
|
|
||||||
"ext-json": "*",
|
|
||||||
"lib-openssl": "*",
|
|
||||||
"php": ">=5.5.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": ">=3.7",
|
|
||||||
"satooshi/php-coveralls": ">=0.6",
|
|
||||||
"squizlabs/php_codesniffer": "2.*"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"classmap": [
|
|
||||||
"src/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"GPL-3.0+"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Joshua Atkins",
|
|
||||||
"email": "joshua.atkins@jublo.net",
|
|
||||||
"homepage": "http://atkins.im/",
|
|
||||||
"role": "Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "J.M.",
|
|
||||||
"email": "jm@jublo.net",
|
|
||||||
"homepage": "http://mynetx.net/",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Easy access to the Twitter REST API, Collections API, Streaming API, TON (Object Nest) API and Twitter Ads API — all from one PHP library.",
|
|
||||||
"homepage": "https://www.jublo.net/projects/codebird/php",
|
|
||||||
"keywords": [
|
|
||||||
"api",
|
|
||||||
"networking",
|
|
||||||
"twitter"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"email": "support@jublo.net",
|
|
||||||
"issues": "https://github.com/jublonet/codebird-php/issues",
|
|
||||||
"source": "https://github.com/jublonet/codebird-php/releases"
|
|
||||||
},
|
|
||||||
"time": "2016-02-15T18:38:55+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "maxmind-db/reader",
|
"name": "maxmind-db/reader",
|
||||||
"version": "v1.11.0",
|
"version": "v1.11.0",
|
||||||
|
@ -1967,5 +1752,5 @@
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": [],
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.2.0"
|
"plugin-api-version": "2.3.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 66a35f030f9eec02d8e51710c7de2161d2a1796f
|
Subproject commit 6e099040138fb0f53d0d179a740c0a1ec5bd5258
|
|
@ -108,6 +108,8 @@ define('MSZ_STORAGE', $cfg->getValue('storage.path', CfgType::T_STR, MSZ_ROOT .
|
||||||
if(!is_dir(MSZ_STORAGE))
|
if(!is_dir(MSZ_STORAGE))
|
||||||
mkdir(MSZ_STORAGE, 0775, true);
|
mkdir(MSZ_STORAGE, 0775, true);
|
||||||
|
|
||||||
|
$ctx = new MszContext($db, $cfg);
|
||||||
|
|
||||||
if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
||||||
if(realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
|
if(realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
|
||||||
if(($argv[1] ?? '') === 'cron' && ($argv[2] ?? '') === 'low')
|
if(($argv[1] ?? '') === 'cron' && ($argv[2] ?? '') === 'low')
|
||||||
|
@ -118,8 +120,6 @@ if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ctx = new MszContext($db, $cfg);
|
|
||||||
|
|
||||||
// Everything below here should eventually be moved to index.php, probably only initialised when required.
|
// Everything below here should eventually be moved to index.php, probably only initialised when required.
|
||||||
// Serving things like the css/js doesn't need to initialise sessions.
|
// Serving things like the css/js doesn't need to initialise sessions.
|
||||||
|
|
||||||
|
|
3
msz
3
msz
|
@ -12,9 +12,8 @@ if(!MSZ_CLI)
|
||||||
|
|
||||||
$commands = new CommandCollection;
|
$commands = new CommandCollection;
|
||||||
$commands->addCommands(
|
$commands->addCommands(
|
||||||
new \Misuzu\Console\Commands\CronCommand,
|
new \Misuzu\Console\Commands\CronCommand($ctx),
|
||||||
new \Misuzu\Console\Commands\MigrateCommand,
|
new \Misuzu\Console\Commands\MigrateCommand,
|
||||||
new \Misuzu\Console\Commands\NewMigrationCommand,
|
new \Misuzu\Console\Commands\NewMigrationCommand,
|
||||||
new \Misuzu\Console\Commands\TwitterAuthCommand,
|
|
||||||
);
|
);
|
||||||
$commands->dispatch(new CommandArgs($argv));
|
$commands->dispatch(new CommandArgs($argv));
|
||||||
|
|
61
public/manage/general/twitter.php
Normal file
61
public/manage/general/twitter.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use Misuzu\Config\CfgType;
|
||||||
|
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 = $ctx->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,16 +53,11 @@ if(!empty($_POST['post']) && CSRF::validateRequest()) {
|
||||||
|
|
||||||
if(!empty($isNew)) {
|
if(!empty($isNew)) {
|
||||||
if($postInfo->isFeatured()) {
|
if($postInfo->isFeatured()) {
|
||||||
$twitterApiKey = $cfg->getValue('twitter.api.key', CfgType::T_STR);
|
$twitter = $ctx->createTwitterClient();
|
||||||
$twitterApiSecret = $cfg->getValue('twitter.api.secret', CfgType::T_STR);
|
|
||||||
$twitterToken = $cfg->getValue('twitter.token.key', CfgType::T_STR);
|
|
||||||
$twitterTokenSecret = $cfg->getValue('twitter.token.secret', CfgType::T_STR);
|
|
||||||
|
|
||||||
if(!empty($twitterApiKey) && !empty($twitterApiSecret)
|
if($twitter->hasAccessToken()) {
|
||||||
&& !empty($twitterToken) && !empty($twitterTokenSecret)) {
|
|
||||||
Twitter::init($twitterApiKey, $twitterApiSecret, $twitterToken, $twitterTokenSecret);
|
|
||||||
$url = url('news-post', ['post' => $postInfo->getId()]);
|
$url = url('news-post', ['post' => $postInfo->getId()]);
|
||||||
Twitter::sendTweet("News :: {$postInfo->getTitle()}\nhttps://{$_SERVER['HTTP_HOST']}{$url}");
|
$twitter->sendTweet("News :: {$postInfo->getTitle()}\nhttps://{$_SERVER['HTTP_HOST']}{$url}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Config;
|
namespace Misuzu\Config;
|
||||||
|
|
||||||
|
// getValue (and hasValue?) should probably be replaced with something that allows grouped loading
|
||||||
|
// that way the entire config doesn't have to be kept in memory on every request
|
||||||
|
// this probably has to be delayed until the backwards compat static Config object isn't needed anymore
|
||||||
|
// otherwise there'd be increased overhead
|
||||||
|
// bulk operations for setValue and removeValue would also be cool
|
||||||
|
|
||||||
interface IConfig {
|
interface IConfig {
|
||||||
function scopeTo(string $prefix): IConfig;
|
function scopeTo(string $prefix): IConfig;
|
||||||
function getNames(): array;
|
function getNames(): array;
|
||||||
|
|
|
@ -2,10 +2,18 @@
|
||||||
namespace Misuzu\Console\Commands;
|
namespace Misuzu\Console\Commands;
|
||||||
|
|
||||||
use Misuzu\DB;
|
use Misuzu\DB;
|
||||||
|
use Misuzu\MszContext;
|
||||||
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 MszContext $context;
|
||||||
|
|
||||||
|
public function __construct(MszContext $ctx) {
|
||||||
|
$this->context = $ctx;
|
||||||
|
}
|
||||||
|
|
||||||
public function getName(): string {
|
public function getName(): string {
|
||||||
return 'cron';
|
return 'cron';
|
||||||
}
|
}
|
||||||
|
@ -26,13 +34,25 @@ class CronCommand implements CommandInterface {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'func':
|
case 'func':
|
||||||
call_user_func($task['command']);
|
call_user_func([$this, $task['command']]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncForumStats(): void {
|
||||||
|
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.',
|
||||||
|
@ -141,7 +161,7 @@ class CronCommand implements CommandInterface {
|
||||||
'name' => 'Recount forum topics and posts.',
|
'name' => 'Recount forum topics and posts.',
|
||||||
'type' => 'func',
|
'type' => 'func',
|
||||||
'slow' => true,
|
'slow' => true,
|
||||||
'command' => 'forum_count_synchronise',
|
'command' => 'syncForumStats',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Clean up expired tfa tokens.',
|
'name' => 'Clean up expired tfa tokens.',
|
||||||
|
@ -151,5 +171,11 @@ 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',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu\Console\Commands;
|
|
||||||
|
|
||||||
use Misuzu\Config;
|
|
||||||
use Misuzu\Config\CfgType;
|
|
||||||
use Misuzu\Twitter;
|
|
||||||
use Misuzu\Console\CommandArgs;
|
|
||||||
use Misuzu\Console\CommandInterface;
|
|
||||||
|
|
||||||
class TwitterAuthCommand implements CommandInterface {
|
|
||||||
public function getName(): string {
|
|
||||||
return 'twitter-auth';
|
|
||||||
}
|
|
||||||
public function getSummary(): string {
|
|
||||||
return 'Creates Twitter authentication tokens.';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dispatch(CommandArgs $args): void {
|
|
||||||
$apiKey = Config::get('twitter.api.key', CfgType::T_STR);
|
|
||||||
$apiSecret = Config::get('twitter.api.secret', CfgType::T_STR);
|
|
||||||
|
|
||||||
if(empty($apiKey) || empty($apiSecret)) {
|
|
||||||
echo 'No Twitter api keys set in config.' . PHP_EOL;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Twitter::init($apiKey, $apiSecret);
|
|
||||||
echo 'Twitter Authentication' . PHP_EOL;
|
|
||||||
|
|
||||||
$authPage = Twitter::createAuth();
|
|
||||||
|
|
||||||
if(empty($authPage)) {
|
|
||||||
echo 'Request to begin authentication failed.' . PHP_EOL;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo 'Go to the page below and paste the pin code displayed.' . PHP_EOL . $authPage . PHP_EOL;
|
|
||||||
|
|
||||||
$pin = readline('Pin: ');
|
|
||||||
$authComplete = Twitter::completeAuth($pin);
|
|
||||||
|
|
||||||
if(empty($authComplete)) {
|
|
||||||
echo 'Invalid pin code.' . PHP_EOL;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo 'Authentication successful!' . PHP_EOL
|
|
||||||
. "Token: {$authComplete['token']}" . PHP_EOL
|
|
||||||
. "Token Secret: {$authComplete['token_secret']}" . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,8 @@ use Misuzu\Template;
|
||||||
use Misuzu\Config\IConfig;
|
use Misuzu\Config\IConfig;
|
||||||
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\Http\HttpFx;
|
use Index\Http\HttpFx;
|
||||||
use Index\Http\HttpRequest;
|
use Index\Http\HttpRequest;
|
||||||
|
@ -42,6 +44,10 @@ class MszContext {
|
||||||
return $this->users;
|
return $this->users;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -111,6 +117,7 @@ class MszContext {
|
||||||
$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,59 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Misuzu;
|
|
||||||
|
|
||||||
use Codebird\Codebird;
|
|
||||||
|
|
||||||
final class Twitter {
|
|
||||||
public static function init(
|
|
||||||
string $apiKey,
|
|
||||||
string $apiSecretKey,
|
|
||||||
?string $token = null,
|
|
||||||
?string $tokenSecret = null
|
|
||||||
): void {
|
|
||||||
Codebird::setConsumerKey($apiKey, $apiSecretKey);
|
|
||||||
|
|
||||||
if($token !== null && $tokenSecret !== null) {
|
|
||||||
self::setToken($token, $tokenSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function setToken(string $token, string $tokenSecret): void {
|
|
||||||
Codebird::getInstance()->setToken($token, $tokenSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function createAuth(): ?string {
|
|
||||||
$codebird = Codebird::getInstance();
|
|
||||||
$reply = $codebird->oauth_requestToken([
|
|
||||||
'oauth_callback' => 'oob',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if(!$reply)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
self::setToken($reply->oauth_token, $reply->oauth_token_secret);
|
|
||||||
|
|
||||||
return $codebird->oauth_authorize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function completeAuth(string $pin): array {
|
|
||||||
$reply = Codebird::getInstance()->oauth_accessToken([
|
|
||||||
'oauth_verifier' => $pin,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if(!$reply)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
self::setToken($reply->oauth_token, $reply->oauth_token_secret);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'token' => $reply->oauth_token,
|
|
||||||
'token_secret' => $reply->oauth_token_secret,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function sendTweet(string $text): void {
|
|
||||||
Codebird::getInstance()->statuses_update([
|
|
||||||
'status' => $text,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
92
src/Twitter/TwitterAccessToken.php
Normal file
92
src/Twitter/TwitterAccessToken.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Twitter;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
use Misuzu\Config\IConfig;
|
||||||
|
use Misuzu\Config\CfgType;
|
||||||
|
|
||||||
|
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', CfgType::T_STR),
|
||||||
|
$config->getValue('token', CfgType::T_STR),
|
||||||
|
$config->getValue('expires', CfgType::T_INT),
|
||||||
|
$config->getValue('token', CfgType::T_ARR),
|
||||||
|
$config->getValue('refresh', CfgType::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');
|
||||||
|
}
|
||||||
|
}
|
115
src/Twitter/TwitterAuthorisation.php
Normal file
115
src/Twitter/TwitterAuthorisation.php
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
188
src/Twitter/TwitterClient.php
Normal file
188
src/Twitter/TwitterClient.php
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
<?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'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
36
src/Twitter/TwitterClientId.php
Normal file
36
src/Twitter/TwitterClientId.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Twitter;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
use Misuzu\Config\IConfig;
|
||||||
|
use Misuzu\Config\CfgType;
|
||||||
|
|
||||||
|
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', CfgType::T_STR),
|
||||||
|
$config->getValue('clientSecret', CfgType::T_STR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
53
src/Twitter/TwitterRoutes.php
Normal file
53
src/Twitter/TwitterRoutes.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Twitter;
|
||||||
|
|
||||||
|
use Index\Http\HttpFx;
|
||||||
|
use Misuzu\MszContext;
|
||||||
|
use Misuzu\Config\IConfig;
|
||||||
|
use Misuzu\Twitter\TwitterAccessToken;
|
||||||
|
use Misuzu\Twitter\TwitterAuthorisation;
|
||||||
|
use Misuzu\Twitter\TwitterClient;
|
||||||
|
use Misuzu\Twitter\TwitterClientId;
|
||||||
|
|
||||||
|
final class TwitterRoutes {
|
||||||
|
private MszContext $context;
|
||||||
|
private IConfig $config;
|
||||||
|
private ?TwitterClientId $clientId = null;
|
||||||
|
|
||||||
|
public function __construct(MszContext $ctx, HttpFx $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'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ function manage_get_menu(int $userId): array {
|
||||||
$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_BLACKLIST))
|
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_BLACKLIST))
|
||||||
$menu['General']['IP Blacklist'] = url('manage-general-blacklist');
|
$menu['General']['IP Blacklist'] = url('manage-general-blacklist');
|
||||||
|
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');
|
||||||
|
@ -147,6 +149,11 @@ function manage_perms_list(array $rawPerms): array {
|
||||||
'title' => 'Can manage blacklistings.',
|
'title' => 'Can manage blacklistings.',
|
||||||
'perm' => MSZ_PERM_GENERAL_MANAGE_BLACKLIST,
|
'perm' => MSZ_PERM_GENERAL_MANAGE_BLACKLIST,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'section' => 'manage-twitter',
|
||||||
|
'title' => 'Can manage Twitter connection.',
|
||||||
|
'perm' => MSZ_PERM_GENERAL_MANAGE_TWITTER,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
|
@ -6,6 +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);
|
define('MSZ_PERM_GENERAL_IS_TESTER', 0x00000010);
|
||||||
define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020);
|
define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020);
|
||||||
|
define('MSZ_PERM_GENERAL_MANAGE_TWITTER', 0x00000040);
|
||||||
|
|
||||||
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,11 +85,14 @@ 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-blacklist' => ['/manage/general/blacklist.php'],
|
'manage-general-blacklist' => ['/manage/general/blacklist.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>']],
|
||||||
|
|
40
templates/manage/general/twitter.twig
Normal file
40
templates/manage/general/twitter.twig
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% 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 %}
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
use \Index\Colour\Colour;
|
use Index\Colour\Colour;
|
||||||
|
|
||||||
function array_test(array $array, callable $func): bool {
|
function array_test(array $array, callable $func): bool {
|
||||||
foreach($array as $value)
|
foreach($array as $value)
|
||||||
|
|
Loading…
Reference in a new issue