|
|
| (4 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| == 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.<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" (called on line 59 of Discord.php) 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>Hooks are registered when described in the extension's <code>extension.json</code> 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):<syntaxhighlight lang="php" start="488" line="1">
| |
| 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 ] );
| |
| }
| |
| </syntaxhighlight>
| |
|
| |
| == Module pages in English and Portuguese instances produces errors == | | == Module pages in English and Portuguese instances produces errors == |
| Full backtrace: | | Full backtrace: |
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
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;
}