ProleWiki:Tasks/Technical: Difference between revisions
From ProleWiki, the proletarian encyclopedia
More languages
More actions
(Added further context for mText variable) Tag: Visual edit |
(Detailed the functioning of the Discord extension, so that we can study it and implement for ConfirmAccount) Tag: Visual edit |
||
Line 1: | Line 1: | ||
=== Module pages in English and Portuguese instances produces errors | == Notification system for ConfirmAccount extension == | ||
The following is to detail how the Discord extension works, in hopes to implement that on the ConfirmAccount extension. | |||
=== Discord.php === | |||
Uses a MediaWiki hook to trigger the Discord event. So we'd need to check for ConfirmAccount hooks, or worse, create one.<syntaxhighlight lang="php" start="16" line="1"> | |||
/** | |||
* Called when a page is created or edited | |||
* @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete | |||
*/ | |||
public static function onPageSaveComplete( WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revision, EditResult $editResult ) { | |||
global $wgDiscordNoBots, $wgDiscordNoMinor, $wgDiscordNoNull; | |||
$hookName = 'PageContentSaveComplete'; | |||
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity ); | |||
if ( DiscordUtils::isDisabled( $hookName, $wikiPage->getTitle()->getNamespace(), $user ) ) { | |||
return true; | |||
} | |||
if ( $wgDiscordNoBots && $user->isBot() ) { | |||
// Don't continue, this is a bot edit | |||
return true; | |||
} | |||
if ( $wgDiscordNoMinor && $revision->isMinor() ) { | |||
// Don't continue, this is a minor edit | |||
return true; | |||
} | |||
if ( $wgDiscordNoNull && $editResult->isNullEdit() ) { | |||
// Don't continue, this is a null edit | |||
return true; | |||
} | |||
$isNew = $editResult->isNew(); | |||
if ( $wikiPage->getTitle()->inNamespace( NS_FILE ) && $isNew ) { | |||
// Don't continue, it's a new file which onUploadComplete will handle instead | |||
return true; | |||
} | |||
$msgKey = 'discord-edit'; | |||
if ( $isNew ) { // is a new page | |||
$msgKey = 'discord-create'; | |||
} | |||
$msg = wfMessage( $msgKey, DiscordUtils::createUserLinks( $user ), | |||
DiscordUtils::createMarkdownLink( $wikiPage->getTitle(), $wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ), | |||
DiscordUtils::createRevisionText( $revision ), | |||
( $summary ? ('`' . DiscordUtils::sanitiseText( DiscordUtils::truncateText( $summary ) ) . '`' ) : '' ) )->plain(); | |||
DiscordUtils::handleDiscord($hookName, $msg); | |||
return true; | |||
} | |||
</syntaxhighlight> | |||
=== Utils.php === | |||
Further detailing of the "handleDiscord" method, which contains the logic for communication with Discord<syntaxhighlight lang="php" line="1" start="47"> | |||
/** | |||
* Handles sending a webhook to Discord using cURL | |||
*/ | |||
public static function handleDiscord ($hookName, $msg) { | |||
global $wgDiscordWebhookURL, $wgDiscordEmojis, $wgDiscordUseEmojis, $wgDiscordPrependTimestamp, $wgDiscordUseFileGetContents; | |||
if ( !$wgDiscordWebhookURL ) { | |||
// There's nothing in here, so we won't do anything | |||
return false; | |||
} | |||
$urls = []; | |||
if ( is_array( $wgDiscordWebhookURL ) ) { | |||
$urls = array_merge($urls, $wgDiscordWebhookURL); | |||
} else if ( is_string($wgDiscordWebhookURL) ) { | |||
$urls[] = $wgDiscordWebhookURL; | |||
} else { | |||
wfDebugLog( 'discord', 'The value of $wgDiscordWebhookURL is not valid and therefore no webhooks could be sent.' ); | |||
return false; | |||
} | |||
// Strip whitespace to just one space | |||
$stripped = preg_replace('/\s+/', ' ', $msg); | |||
if ( $wgDiscordPrependTimestamp ) { | |||
// Add timestamp | |||
$dateString = gmdate( wfMessage( 'discord-timestampformat' )->text() ); | |||
$stripped = $dateString . ' ' . $stripped; | |||
} | |||
if ( $wgDiscordUseEmojis ) { | |||
// Add emoji | |||
$emoji = $wgDiscordEmojis[$hookName]; | |||
$stripped = $emoji . ' ' . $stripped; | |||
} | |||
DeferredUpdates::addCallableUpdate( function() use ( $stripped, $urls, $wgDiscordUseFileGetContents ) { | |||
$user_agent = 'mw-discord/1.0 (github.com/jaydenkieran)'; | |||
$json_data = [ | |||
'content' => "$stripped", | |||
'allowed_mentions' => [ | |||
'parse' => [] | |||
] | |||
]; | |||
$json = json_encode($json_data); | |||
if ( $wgDiscordUseFileGetContents ) { | |||
// They want to use file_get_contents | |||
foreach ($urls as &$value) { | |||
$contextOpts = [ | |||
'http' => [ | |||
'header' => 'Content-Type: application/x-www-form-urlencoded', | |||
'method' => 'POST', // Send as a POST request | |||
'user_agent' => $user_agent, // Add a unique user agent | |||
'content' => $json, // Send the JSON in the POST request | |||
'ignore_errors' => true // If the call fails, let's not do anything with it | |||
] | |||
]; | |||
$context = stream_context_create( $contextOpts ); | |||
$result = file_get_contents( $value, false, $context ); | |||
} | |||
} else { | |||
// By default, we use cURL | |||
// Set up cURL multi handlers | |||
$c_handlers = []; | |||
$result = []; | |||
$mh = curl_multi_init(); | |||
foreach ($urls as &$value) { | |||
$c_handlers[$value] = curl_init( $value ); | |||
curl_setopt( $c_handlers[$value], CURLOPT_POST, 1 ); // Send as a POST request | |||
curl_setopt( $c_handlers[$value], CURLOPT_POSTFIELDS, $json ); // Send the JSON in the POST request | |||
curl_setopt( $c_handlers[$value], CURLOPT_FOLLOWLOCATION, 1 ); | |||
curl_setopt( $c_handlers[$value], CURLOPT_HEADER, 0 ); | |||
curl_setopt( $c_handlers[$value], CURLOPT_RETURNTRANSFER, 1 ); | |||
curl_setopt( $c_handlers[$value], CURLOPT_CONNECTTIMEOUT, 10 ); // Add a timeout for connecting to the site | |||
curl_setopt( $c_handlers[$value], CURLOPT_TIMEOUT, 10 ); // Do not allow cURL to run for a long time | |||
curl_setopt( $c_handlers[$value], CURLOPT_USERAGENT, $user_agent ); // Add a unique user agent | |||
curl_setopt( $c_handlers[$value], CURLOPT_HTTPHEADER, array( | |||
'Content-Type: application/json' | |||
)); | |||
curl_multi_add_handle( $mh, $c_handlers[$value] ); | |||
} | |||
$running = null; | |||
do { | |||
curl_multi_exec($mh, $running); | |||
} while ($running); | |||
// Remove all handlers and then close the multi handler | |||
foreach($c_handlers as $k => $ch) { | |||
$result[$k] = curl_multi_getcontent($ch); | |||
wfDebugLog( 'discord', 'Result of cURL was: ' . $result[$k] ); | |||
curl_multi_remove_handle($mh, $ch); | |||
} | |||
curl_multi_close($mh); | |||
} | |||
} ); | |||
return true; | |||
} | |||
</syntaxhighlight> | |||
== Module pages in English and Portuguese instances produces errors == | |||
Full backtrace: | Full backtrace: | ||
<pre>LogicException: This ParserOutput contains no text! | <pre>LogicException: This ParserOutput contains no text! |
Revision as of 03:00, 29 April 2024
Notification system for ConfirmAccount extension
The following is to detail how the Discord extension works, in hopes to implement that on the ConfirmAccount extension.
Discord.php
Uses a MediaWiki hook to trigger the Discord event. So we'd need to check for ConfirmAccount hooks, or worse, create one.
/**
* Called when a page is created or edited
* @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
*/
public static function onPageSaveComplete( WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revision, EditResult $editResult ) {
global $wgDiscordNoBots, $wgDiscordNoMinor, $wgDiscordNoNull;
$hookName = 'PageContentSaveComplete';
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
if ( DiscordUtils::isDisabled( $hookName, $wikiPage->getTitle()->getNamespace(), $user ) ) {
return true;
}
if ( $wgDiscordNoBots && $user->isBot() ) {
// Don't continue, this is a bot edit
return true;
}
if ( $wgDiscordNoMinor && $revision->isMinor() ) {
// Don't continue, this is a minor edit
return true;
}
if ( $wgDiscordNoNull && $editResult->isNullEdit() ) {
// Don't continue, this is a null edit
return true;
}
$isNew = $editResult->isNew();
if ( $wikiPage->getTitle()->inNamespace( NS_FILE ) && $isNew ) {
// Don't continue, it's a new file which onUploadComplete will handle instead
return true;
}
$msgKey = 'discord-edit';
if ( $isNew ) { // is a new page
$msgKey = 'discord-create';
}
$msg = wfMessage( $msgKey, DiscordUtils::createUserLinks( $user ),
DiscordUtils::createMarkdownLink( $wikiPage->getTitle(), $wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
DiscordUtils::createRevisionText( $revision ),
( $summary ? ('`' . DiscordUtils::sanitiseText( DiscordUtils::truncateText( $summary ) ) . '`' ) : '' ) )->plain();
DiscordUtils::handleDiscord($hookName, $msg);
return true;
}
Utils.php
Further detailing of the "handleDiscord" method, which contains the logic for communication with Discord
/**
* Handles sending a webhook to Discord using cURL
*/
public static function handleDiscord ($hookName, $msg) {
global $wgDiscordWebhookURL, $wgDiscordEmojis, $wgDiscordUseEmojis, $wgDiscordPrependTimestamp, $wgDiscordUseFileGetContents;
if ( !$wgDiscordWebhookURL ) {
// There's nothing in here, so we won't do anything
return false;
}
$urls = [];
if ( is_array( $wgDiscordWebhookURL ) ) {
$urls = array_merge($urls, $wgDiscordWebhookURL);
} else if ( is_string($wgDiscordWebhookURL) ) {
$urls[] = $wgDiscordWebhookURL;
} else {
wfDebugLog( 'discord', 'The value of $wgDiscordWebhookURL is not valid and therefore no webhooks could be sent.' );
return false;
}
// Strip whitespace to just one space
$stripped = preg_replace('/\s+/', ' ', $msg);
if ( $wgDiscordPrependTimestamp ) {
// Add timestamp
$dateString = gmdate( wfMessage( 'discord-timestampformat' )->text() );
$stripped = $dateString . ' ' . $stripped;
}
if ( $wgDiscordUseEmojis ) {
// Add emoji
$emoji = $wgDiscordEmojis[$hookName];
$stripped = $emoji . ' ' . $stripped;
}
DeferredUpdates::addCallableUpdate( function() use ( $stripped, $urls, $wgDiscordUseFileGetContents ) {
$user_agent = 'mw-discord/1.0 (github.com/jaydenkieran)';
$json_data = [
'content' => "$stripped",
'allowed_mentions' => [
'parse' => []
]
];
$json = json_encode($json_data);
if ( $wgDiscordUseFileGetContents ) {
// They want to use file_get_contents
foreach ($urls as &$value) {
$contextOpts = [
'http' => [
'header' => 'Content-Type: application/x-www-form-urlencoded',
'method' => 'POST', // Send as a POST request
'user_agent' => $user_agent, // Add a unique user agent
'content' => $json, // Send the JSON in the POST request
'ignore_errors' => true // If the call fails, let's not do anything with it
]
];
$context = stream_context_create( $contextOpts );
$result = file_get_contents( $value, false, $context );
}
} else {
// By default, we use cURL
// Set up cURL multi handlers
$c_handlers = [];
$result = [];
$mh = curl_multi_init();
foreach ($urls as &$value) {
$c_handlers[$value] = curl_init( $value );
curl_setopt( $c_handlers[$value], CURLOPT_POST, 1 ); // Send as a POST request
curl_setopt( $c_handlers[$value], CURLOPT_POSTFIELDS, $json ); // Send the JSON in the POST request
curl_setopt( $c_handlers[$value], CURLOPT_FOLLOWLOCATION, 1 );
curl_setopt( $c_handlers[$value], CURLOPT_HEADER, 0 );
curl_setopt( $c_handlers[$value], CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $c_handlers[$value], CURLOPT_CONNECTTIMEOUT, 10 ); // Add a timeout for connecting to the site
curl_setopt( $c_handlers[$value], CURLOPT_TIMEOUT, 10 ); // Do not allow cURL to run for a long time
curl_setopt( $c_handlers[$value], CURLOPT_USERAGENT, $user_agent ); // Add a unique user agent
curl_setopt( $c_handlers[$value], CURLOPT_HTTPHEADER, array(
'Content-Type: application/json'
));
curl_multi_add_handle( $mh, $c_handlers[$value] );
}
$running = null;
do {
curl_multi_exec($mh, $running);
} while ($running);
// Remove all handlers and then close the multi handler
foreach($c_handlers as $k => $ch) {
$result[$k] = curl_multi_getcontent($ch);
wfDebugLog( 'discord', 'Result of cURL was: ' . $result[$k] );
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
}
} );
return true;
}
Module pages in English and Portuguese instances produces errors
Full backtrace:
LogicException: This ParserOutput contains no text! Backtrace: from /var/www/prolewiki/includes/parser/ParserOutput.php(382) #0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText() #1 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(193): MediaWiki\Extension\Scribunto\ScribuntoContentHandler->highlight() #2 /var/www/prolewiki/includes/content/ContentHandler.php(1759): MediaWiki\Extension\Scribunto\ScribuntoContentHandler->fillParserOutput() #3 /var/www/prolewiki/includes/content/Renderer/ContentRenderer.php(47): ContentHandler->getParserOutput() #4 /var/www/prolewiki/includes/Revision/RenderedRevision.php(259): MediaWiki\Content\Renderer\ContentRenderer->getParserOutput() #5 /var/www/prolewiki/includes/Revision/RenderedRevision.php(232): MediaWiki\Revision\RenderedRevision->getSlotParserOutputUncached() #6 /var/www/prolewiki/includes/Revision/RevisionRenderer.php(223): MediaWiki\Revision\RenderedRevision->getSlotParserOutput() #7 /var/www/prolewiki/includes/Revision/RevisionRenderer.php(164): MediaWiki\Revision\RevisionRenderer->combineSlotOutput() #8 [internal function]: MediaWiki\Revision\RevisionRenderer->MediaWiki\Revision\{closure}() #9 /var/www/prolewiki/includes/Revision/RenderedRevision.php(199): call_user_func() #10 /var/www/prolewiki/includes/poolcounter/PoolWorkArticleView.php(84): MediaWiki\Revision\RenderedRevision->getRevisionParserOutput() #11 /var/www/prolewiki/includes/poolcounter/PoolWorkArticleView.php(69): PoolWorkArticleView->renderRevision() #12 /var/www/prolewiki/includes/poolcounter/PoolCounterWork.php(167): PoolWorkArticleView->doWork() #13 /var/www/prolewiki/includes/page/ParserOutputAccess.php(304): PoolCounterWork->execute() #14 /var/www/prolewiki/includes/page/Article.php(746): MediaWiki\Page\ParserOutputAccess->getParserOutput() #15 /var/www/prolewiki/includes/page/Article.php(550): Article->generateContentOutput() #16 /var/www/prolewiki/includes/actions/ViewAction.php(78): Article->view() #17 /var/www/prolewiki/includes/MediaWiki.php(583): ViewAction->show() #18 /var/www/prolewiki/includes/MediaWiki.php(363): MediaWiki->performAction() #19 /var/www/prolewiki/includes/MediaWiki.php(960): MediaWiki->performRequest() #20 /var/www/prolewiki/includes/MediaWiki.php(613): MediaWiki->main() #21 /var/www/prolewiki/index.php(50): MediaWiki->run() #22 /var/www/prolewiki/index.php(46): wfIndexMain() #23 {main}
from /var/www/prolewiki/includes/parser/ParserOutput.php (382)
public function getRawText() {
if ( $this->mText === null ) {
throw new LogicException( 'This ParserOutput contains no text!' );
}
return $this->mText;
}
Further context on mText
private $mText = null;
public function __construct( $text = null, $languageLinks = [], $categoryLinks = [],
$unused = false, $titletext = ''
) {
$this->mText = $text;
$this->mLanguageLinks = $languageLinks;
$this->mCategories = $categoryLinks;
$this->mTitleText = $titletext;
}
public function setText( $text ) {
return wfSetVar( $this->mText, $text, true );
}
#0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText()
protected function highlight( $text, ParserOutput $parserOutput, ScribuntoEngineBase $engine ) {
global $wgScribuntoUseGeSHi;
$language = $engine->getGeSHiLanguage();
if (
$wgScribuntoUseGeSHi && $language && ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' )
) {
$status = SyntaxHighlight::highlight( $text, $language, [ 'line' => true, 'linelinks' => 'L' ] );
if ( $status->isGood() ) {
// @todo replace addModuleStyles line with the appropriate call on
// SyntaxHighlight once one is created
$parserOutput->addModuleStyles( [ 'ext.pygments' ] );
$parserOutput->addModules( [ 'ext.pygments.linenumbers' ] );
$parserOutput->setText( $parserOutput->getRawText() . $status->getValue() );
return true;
}
}
return false;
}