More languages
More actions
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 extension as reference for Discord notification
There are two main files (Discord.php and DiscordUtils.php) that serve the Discord extension, and together they compose the logic of the notification system.
Discord.php
This example function uses a MediaWiki hook when a page is saved to trigger the Discord event. We probably need to create a hook for the ConfirmAccount extension for it to communicate with the Discord extension.
/**
* 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" (called on line 59 of Discord.php) 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;
}
Hooks are registered when described in the extension's extension.json
file. Both built-in and custom extension hooks are defined there, but custom hooks don't need to be described there necessarily, as far as I've looked.
The ApprovedRevs extension register custom hooks this way (lines 513,514):
public static function saveApprovedRevIDInDB( $title, $rev_id, User $user, $isAutoApprove = true ) {
$timestamp = date( 'Y-m-d H:i:s' );
$approvalInfo = [
'rev_id' => $rev_id,
'approval_date' => $timestamp
];
if ( !$isAutoApprove ) {
$approvalInfo['approver_id'] = $user->getID();
}
$dbw = wfGetDB( DB_MASTER );
$page_id = $title->getArticleID();
$old_rev_id = $dbw->selectField( 'approved_revs', 'rev_id', [ 'page_id' => $page_id ] );
if ( $old_rev_id ) {
$dbw->update( 'approved_revs', $approvalInfo, [ 'page_id' => $page_id ] );
} else {
$approvalInfo['page_id'] = $page_id;
$dbw->insert( 'approved_revs', $approvalInfo );
}
// Update "cache" in memory
self::$mApprovedRevIDForPage[$page_id] = $rev_id;
self::$mApproverForPage[$page_id] = $user;
$content = self::getContent( $title, $rev_id );
MediaWikiServices::getInstance()->getHookContainer()
->run( 'ApprovedRevsRevisionApproved', [ $output = null, $title, $rev_id, $content ] );
}
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;
}