Removed Twitter integrations.

This commit is contained in:
flash 2023-03-09 21:38:03 +00:00
parent 521a8fb0d1
commit d05046ff1f
17 changed files with 2 additions and 638 deletions

View File

@ -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
## 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.
- [@flashwahaha](https://twitter.com/flashwahaha): Twitter of the owner. This is a personal space, proceed with caution!
## Source Code
- [Misuzu](https://git.flash.moe/flashii/misuzu): Backend of the main website.

View File

@ -152,7 +152,6 @@ Template::set('globals', [
'site_name' => $cfg->getValue('site.name', IConfig::T_STR, 'Misuzu'),
'site_description' => $cfg->getValue('site.desc', 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),
'eeprom' => [
'path' => $cfg->getValue('eeprom.path', IConfig::T_STR),

View File

@ -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,
]);

View File

@ -53,12 +53,7 @@ if(!empty($_POST['post']) && CSRF::validateRequest()) {
if(!empty($isNew)) {
if($postInfo->isFeatured()) {
$twitter = $msz->createTwitterClient();
if($twitter->hasAccessToken()) {
$url = url('news-post', ['post' => $postInfo->getId()]);
$twitter->sendTweet("News :: {$postInfo->getTitle()}\nhttps://{$_SERVER['HTTP_HOST']}{$url}");
}
// Twitter integration used to be here, replace with Railgun Pulse integration
}
header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()]));

View File

@ -5,7 +5,6 @@ use Misuzu\DB;
use Misuzu\MisuzuContext;
use Misuzu\Console\CommandArgs;
use Misuzu\Console\CommandInterface;
use Misuzu\Twitter\TwitterAccessToken;
class CronCommand implements CommandInterface {
private MisuzuContext $context;
@ -45,14 +44,6 @@ class CronCommand implements CommandInterface {
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.',
@ -171,11 +162,5 @@ class CronCommand implements CommandInterface {
WHERE `tfa_created` < NOW() - INTERVAL 15 MINUTE
",
],
[
'name' => 'Refresh Twitter authentication token.',
'type' => 'func',
'slow' => true,
'command' => 'refreshTwitterToken',
],
];
}

View File

@ -6,8 +6,6 @@ use Misuzu\Config\IConfig;
use Misuzu\GeoIP\GeoIPHelper;
use Misuzu\SharpChat\SharpChatRoutes;
use Misuzu\Users\Users;
use Misuzu\Twitter\TwitterClient;
use Misuzu\Twitter\TwitterRoutes;
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo;
use Index\Data\Migration\DbMigrationManager;
@ -63,10 +61,6 @@ class MisuzuContext {
return $this->geoIP;
}
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) {
@ -136,7 +130,6 @@ class MisuzuContext {
$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 {

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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'))
);
}
}

View File

@ -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)
);
}
}

View File

@ -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'));
}
}

View File

@ -15,8 +15,6 @@ function manage_get_menu(int $userId): array {
$menu['General']['Emoticons'] = url('manage-general-emoticons');
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_CONFIG))
$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))
$menu['Users & Roles']['Users'] = url('manage-users');
@ -135,11 +133,6 @@ function manage_perms_list(array $rawPerms): array {
'title' => 'Can manage general Misuzu settings.',
'perm' => MSZ_PERM_GENERAL_MANAGE_CONFIG,
],
[
'section' => 'manage-twitter',
'title' => 'Can manage Twitter connection.',
'perm' => MSZ_PERM_GENERAL_MANAGE_TWITTER,
],
],
],
[

View File

@ -6,7 +6,7 @@ define('MSZ_PERM_GENERAL_MANAGE_EMOTES', 0x00000004);
define('MSZ_PERM_GENERAL_MANAGE_CONFIG', 0x00000008);
//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_TWITTER', 0x00000040);
//define('MSZ_PERM_GENERAL_MANAGE_TWITTER', 0x00000040); Twitter integration has been removed
define('MSZ_PERMS_USER', 'user');
define('MSZ_PERM_USER_EDIT_PROFILE', 0x00000001);

View File

@ -85,13 +85,10 @@ 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-twitter' => ['/manage/general/twitter.php'],
'manage-general-emoticons' => ['/manage/general/emoticons.php'],
'manage-general-emoticon' => ['/manage/general/emoticon.php', ['e' => '<emote>']],

View File

@ -1,6 +1,5 @@
{% apply spaceless %}
{% set description = description|default(globals.site_description) %}
{% set site_twitter = site_twitter|default(globals.site_twitter) %}
{% if title is defined %}
{% set browser_title = title ~ ' :: ' ~ globals.site_name %}
@ -10,22 +9,15 @@
<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:site_name" content="{{ globals.site_name }}">
{% if description|length > 0 %}
<meta name="description" content="{{ description }}">
<meta name="twitter:description" content="{{ description }}">
<meta property="og:description" content="{{ description }}">
{% endif %}
{% if site_twitter|length > 0 %}
<meta name="twitter:site" content="{{ site_twitter }}">
{% endif %}
<meta property="og:type" content="object">
<meta name="twitter:card" content="summary">
{% if image is defined %}
{% if image|slice(0, 1) == '/' %}
@ -37,7 +29,6 @@
{% endif %}
{% if image|length > 0 %}
<meta name="twitter:image:src" content="{{ image }}">
<meta property="og:image" content="{{ image }}">
{% endif %}
{% endif %}

View File

@ -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&amp;csrf={{ csrf_token() }}">Refresh Access</a>
{% endif %}
<a class="input__button" href="?m=revoke&amp;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&amp;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 %}

View File

@ -48,11 +48,6 @@
'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,
},
],
},
{