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",
|
||||
"erusev/parsedown": "~1.6",
|
||||
"geoip2/geoip2": "~2.0",
|
||||
"jublonet/codebird-php": "^3.1",
|
||||
"chillerlan/php-qrcode": "^4.3",
|
||||
"whichbrowser/parser": "^2.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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d3c39d122a38515484c9d439ecee240b",
|
||||
"content-hash": "b3b6cf189969ecb1dd838c23a4fe96e1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "chillerlan/php-qrcode",
|
||||
|
@ -223,157 +223,6 @@
|
|||
],
|
||||
"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",
|
||||
"version": "1.2.3",
|
||||
|
@ -626,70 +475,6 @@
|
|||
},
|
||||
"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",
|
||||
"version": "v1.11.0",
|
||||
|
@ -1967,5 +1752,5 @@
|
|||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"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))
|
||||
mkdir(MSZ_STORAGE, 0775, true);
|
||||
|
||||
$ctx = new MszContext($db, $cfg);
|
||||
|
||||
if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
||||
if(realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
|
||||
if(($argv[1] ?? '') === 'cron' && ($argv[2] ?? '') === 'low')
|
||||
|
@ -118,8 +120,6 @@ if(MSZ_CLI) { // Temporary backwards compatibility measure, remove this later
|
|||
return;
|
||||
}
|
||||
|
||||
$ctx = new MszContext($db, $cfg);
|
||||
|
||||
// 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.
|
||||
|
||||
|
|
3
msz
3
msz
|
@ -12,9 +12,8 @@ if(!MSZ_CLI)
|
|||
|
||||
$commands = new CommandCollection;
|
||||
$commands->addCommands(
|
||||
new \Misuzu\Console\Commands\CronCommand,
|
||||
new \Misuzu\Console\Commands\CronCommand($ctx),
|
||||
new \Misuzu\Console\Commands\MigrateCommand,
|
||||
new \Misuzu\Console\Commands\NewMigrationCommand,
|
||||
new \Misuzu\Console\Commands\TwitterAuthCommand,
|
||||
);
|
||||
$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($postInfo->isFeatured()) {
|
||||
$twitterApiKey = $cfg->getValue('twitter.api.key', CfgType::T_STR);
|
||||
$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);
|
||||
$twitter = $ctx->createTwitterClient();
|
||||
|
||||
if(!empty($twitterApiKey) && !empty($twitterApiSecret)
|
||||
&& !empty($twitterToken) && !empty($twitterTokenSecret)) {
|
||||
Twitter::init($twitterApiKey, $twitterApiSecret, $twitterToken, $twitterTokenSecret);
|
||||
if($twitter->hasAccessToken()) {
|
||||
$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
|
||||
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 {
|
||||
function scopeTo(string $prefix): IConfig;
|
||||
function getNames(): array;
|
||||
|
|
|
@ -2,10 +2,18 @@
|
|||
namespace Misuzu\Console\Commands;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\MszContext;
|
||||
use Misuzu\Console\CommandArgs;
|
||||
use Misuzu\Console\CommandInterface;
|
||||
use Misuzu\Twitter\TwitterAccessToken;
|
||||
|
||||
class CronCommand implements CommandInterface {
|
||||
private MszContext $context;
|
||||
|
||||
public function __construct(MszContext $ctx) {
|
||||
$this->context = $ctx;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return 'cron';
|
||||
}
|
||||
|
@ -26,13 +34,25 @@ class CronCommand implements CommandInterface {
|
|||
break;
|
||||
|
||||
case 'func':
|
||||
call_user_func($task['command']);
|
||||
call_user_func([$this, $task['command']]);
|
||||
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 = [
|
||||
[
|
||||
'name' => 'Ensures main role exists.',
|
||||
|
@ -141,7 +161,7 @@ class CronCommand implements CommandInterface {
|
|||
'name' => 'Recount forum topics and posts.',
|
||||
'type' => 'func',
|
||||
'slow' => true,
|
||||
'command' => 'forum_count_synchronise',
|
||||
'command' => 'syncForumStats',
|
||||
],
|
||||
[
|
||||
'name' => 'Clean up expired tfa tokens.',
|
||||
|
@ -151,5 +171,11 @@ class CronCommand implements CommandInterface {
|
|||
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\SharpChat\SharpChatRoutes;
|
||||
use Misuzu\Users\Users;
|
||||
use Misuzu\Twitter\TwitterClient;
|
||||
use Misuzu\Twitter\TwitterRoutes;
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Http\HttpFx;
|
||||
use Index\Http\HttpRequest;
|
||||
|
@ -42,6 +44,10 @@ class MszContext {
|
|||
return $this->users;
|
||||
}*/
|
||||
|
||||
public function createTwitterClient(): TwitterClient {
|
||||
return TwitterClient::create($this->config->scopeTo('twitter'));
|
||||
}
|
||||
|
||||
public function setUpHttp(bool $legacy = false): void {
|
||||
$this->router = new HttpFx;
|
||||
$this->router->use('/', function($response) {
|
||||
|
@ -111,6 +117,7 @@ class MszContext {
|
|||
$this->router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST'));
|
||||
|
||||
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'));
|
||||
new TwitterRoutes($this, $this->router, $this->config->scopeTo('twitter'));
|
||||
}
|
||||
|
||||
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');
|
||||
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_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))
|
||||
$menu['Users & Roles']['Users'] = url('manage-users');
|
||||
|
@ -147,6 +149,11 @@ function manage_perms_list(array $rawPerms): array {
|
|||
'title' => 'Can manage blacklistings.',
|
||||
'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_IS_TESTER', 0x00000010);
|
||||
define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020);
|
||||
define('MSZ_PERM_GENERAL_MANAGE_TWITTER', 0x00000040);
|
||||
|
||||
define('MSZ_PERMS_USER', 'user');
|
||||
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-unpin' => ['/comments.php', ['c' => '<comment>', 'csrf' => '{csrf}', 'm' => 'unpin']],
|
||||
|
||||
'twitter-callback' => ['/_twitter/callback'],
|
||||
|
||||
'manage-index' => ['/manage'],
|
||||
|
||||
'manage-general-overview' => ['/manage/general'],
|
||||
'manage-general-logs' => ['/manage/general/logs.php'],
|
||||
'manage-general-blacklist' => ['/manage/general/blacklist.php'],
|
||||
'manage-general-twitter' => ['/manage/general/twitter.php'],
|
||||
|
||||
'manage-general-emoticons' => ['/manage/general/emoticons.php'],
|
||||
'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
|
||||
use \Index\Colour\Colour;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
function array_test(array $array, callable $func): bool {
|
||||
foreach($array as $value)
|
||||
|
|
Loading…
Reference in a new issue