jenkins-bot has submitted this change and it was merged.

Change subject: Initial commit
......................................................................


Initial commit

TODO:
* Tests
* Don't use varchar in db table (What should we do here, normalize
out reference data?)
* Make filter option show/hide bad edits ("Only show/hide possible
vandalism" or something)
* Support enhanced RC top-level lines--maybe core is lacking the hooks?  Or use
$wgRecentChangesFlags instead of HTML pill.
* Maintenance scripts for cache rebuild and purge.
* Generalize views to show results for other models, e.g. wp10.
* Hook into history view.
* Use structured logging
* Use pool counter

Co-Authored-By: Adam Roses Wight <[email protected]>
Bug: T112856
Change-Id: If3b0f5357d57c8833a31e4e3e03f3284979df10d
---
A .gitignore
A .jscsrc
A .jshintignore
A Gruntfile.js
A README
A composer.json
A extension.json
A i18n/en.json
A i18n/qqq.json
A includes/FetchScoreJob.php
A includes/Hooks.php
A modules/ext.ores.styles.css
A ores.sql
A package.json
A phpcs.xml
15 files changed, 415 insertions(+), 0 deletions(-)

Approvals:
  Legoktm: Looks good to me, approved
  Siebrand: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..53330c7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# Building & testing
+node_modules/
+
+# Composer
+/vendor
+/composer.lock
+/composer.json
diff --git a/.jscsrc b/.jscsrc
new file mode 100644
index 0000000..9d22e3f
--- /dev/null
+++ b/.jscsrc
@@ -0,0 +1,3 @@
+{
+       "preset": "wikimedia"
+}
diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..408d6d9
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,30 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+       grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+       grunt.loadNpmTasks( 'grunt-jsonlint' );
+       grunt.loadNpmTasks( 'grunt-banana-checker' );
+       grunt.loadNpmTasks( 'grunt-jscs' );
+
+       grunt.initConfig( {
+               jshint: {
+                       all: [
+                               '*.js'
+                       ]
+               },
+               jscs: {
+                       src: '<%= jshint.all %>'
+               },
+               banana: {
+                       all: 'i18n/'
+               },
+               jsonlint: {
+                       all: [
+                               '**/*.json',
+                               '!node_modules/**'
+                       ]
+               }
+       } );
+
+       grunt.registerTask( 'test', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] 
);
+       grunt.registerTask( 'default', 'test' );
+};
diff --git a/README b/README
new file mode 100644
index 0000000..ae1ba25
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+New globals:
+
+$wgOresBaseUrl - API endpoint pattern for all ORES requests.  "$wiki" will be
+substituted with your wiki's ID.
+
+$wgOresModels - Array of models we want to fetch from the server and cache in
+the database.
+
+$wgOresRevertTagThresholds - Map from threshold name to score cutoff.  These
+thresholds are used to flag recent changes for potential revert.  Must be
+listed in ascending order (FIXME).
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..534f8fb
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,12 @@
+{
+       "require-dev": {
+               "jakub-onderka/php-parallel-lint": "0.9.*",
+               "mediawiki/mediawiki-codesniffer": "0.3.0"
+       },
+       "scripts": {
+               "test": [
+                       "parallel-lint . --exclude vendor",
+                       "phpcs -p"
+               ]
+       }
+}
diff --git a/extension.json b/extension.json
new file mode 100644
index 0000000..e756ac9
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,64 @@
+{
+       "name": "ORES",
+       "descriptionmsg": "ores-desc",
+       "license-name": "CC0-1.0",
+       "authors": [
+               "Kunal Mehta",
+               "Adam Roses Wight"
+       ],
+       "url": "https://www.mediawiki.org/wiki/Extension:ORES";,
+       "AutoloadClasses": {
+               "ORES\\Hooks": "includes/Hooks.php",
+               "ORES\\FetchScoreJob": "includes/FetchScoreJob.php"
+       },
+       "Hooks": {
+               "LoadExtensionSchemaUpdates": [
+                       "ORES\\Hooks::onLoadExtensionSchemaUpdates"
+               ],
+               "RecentChange_save": [
+                       "ORES\\Hooks::onRecentChange_save"
+               ],
+               "ChangesListSpecialPageFilters": [
+                       "ORES\\Hooks::onChangesListSpecialPageFilters"
+               ],
+               "ChangesListSpecialPageQuery": [
+                       "ORES\\Hooks::onChangesListSpecialPageQuery"
+               ],
+               "EnhancedChangesListModifyLineData": [
+                       "ORES\\Hooks::onEnhancedChangesListModifyLineData"
+               ],
+               "OldChangesListRecentChangesLine": [
+                       "ORES\\Hooks::onOldChangesListRecentChangesLine"
+               ]
+       },
+       "ResourceModules": {
+               "ext.ores.styles": {
+                       "styles": "ext.ores.styles.css",
+                       "position": "top"
+               }
+       },
+       "ResourceFileModulePaths": {
+               "localBasePath": "modules",
+               "remoteExtPath": "ORES/modules"
+       },
+       "MessagesDirs": {
+               "ORES": [
+                       "i18n"
+               ]
+       },
+       "JobClasses": {
+               "ORESFetchScoreJob": "ORES\\FetchScoreJob"
+       },
+       "config": {
+               "OresBaseUrl": "https://ores.wmflabs.org/scores/$wiki/";,
+               "OresModels": [
+                       "reverted"
+               ],
+               "OresRevertTagThresholds": {
+                       "low": 0.8,
+                       "medium": 0.87,
+                       "high": 0.94
+               }
+       },
+       "manifest_version": 1
+}
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..f3d32f5
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,10 @@
+{
+       "@metadata": {
+               "authors": []
+       },
+       "ores-desc": "Expose automated revision scores in the interface",
+       "ores-reverted-filter": "$1 revert predictions",
+       "ores-reverted-high": "High risk, probability of revert $1%",
+       "ores-reverted-medium": "Medium risk, probability of revert $1%",
+       "ores-reverted-low": "Some risk, probability of revert $1%"
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..282c983
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,10 @@
+{
+       "@metadata": {
+               "authors": []
+       },
+       "ores-desc": "Extension summary.",
+       "ores-reverted-filter": "Label to toggle filtering of ORES data. 
Parameters:\n* $1 - Action to be performed by toggling.",
+       "ores-reverted-high": "Title for high probability revert risk. 
Parameters:\n* $1 - Score as a percentage.",
+       "ores-reverted-medium": "Title for medium probability revert risk. 
Parameters:\n* $1 - Score as a percentage.",
+       "ores-reverted-low": "Title for low probability revert risk. 
Parameters:\n* $1 - Score as a percentage."
+}
diff --git a/includes/FetchScoreJob.php b/includes/FetchScoreJob.php
new file mode 100644
index 0000000..00898f5
--- /dev/null
+++ b/includes/FetchScoreJob.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace ORES;
+
+use Job;
+use FormatJson;
+use Title;
+use MWHttpRequest;
+
+class FetchScoreJob extends Job {
+       /**
+        * @param Title $title
+        * @param array $params 'rcid' and 'revid' keys
+        */
+       public function __construct( Title $title, array $params ) {
+               parent::__construct( 'ORESFetchScoreJob', $title, $params );
+       }
+
+       private function getUrl() {
+               global $wgOresBaseUrl, $wgOresModels;
+
+               $url = str_replace( '$wiki', wfWikiID(), $wgOresBaseUrl );
+               $params = array(
+                       'models' => implode( '|', $wgOresModels ),
+                       'revids' => $this->params['revid'],
+               );
+               return wfAppendQuery( $url, $params );
+       }
+
+       public function run() {
+               $url = $this->getUrl();
+               $req = MWHttpRequest::factory( $url, null, __METHOD__ );
+               $status = $req->execute();
+               if ( !$status->isOK() ) {
+                       wfDebugLog( 'ORES', "No response from ORES server at 
$url, "
+                               .  $status->getMessage()->text() );
+                       return false;
+               }
+               $json = $req->getContent();
+               $wireData = FormatJson::decode( $json, true );
+               if ( !$wireData || !empty( $wireData['error'] ) ) {
+                       wfDebugLog( 'ORES', 'Bad response from ORES server: ' . 
$json );
+                       return false;
+               }
+
+               // Map from wire format to database fields.
+               $dbData = array();
+               foreach ( $wireData as $revisionId => $revisionData ) {
+                       foreach ( $revisionData as $model => $modelOutputs ) {
+                               if ( isset( $modelOutputs['error'] ) ) {
+                                       wfDebugLog( 'ORES', 'Model output an 
error: ' . $modelOutputs['error']['message'] );
+                                       return false;
+                               }
+
+                               $prediction = $modelOutputs['prediction'];
+                               // Kludge out booleans so we can match 
prediction against class name.
+                               if ( $prediction === false ) {
+                                       $prediction = 'false';
+                               } elseif ( $prediction === true ) {
+                                       $prediction = 'true';
+                               }
+
+                               foreach ( $modelOutputs['probability'] as 
$class => $probability ) {
+                                       $dbData[] = array(
+                                               'ores_rev' => $revisionId,
+                                               'ores_model' => $model,
+                                               'ores_model_version' => '', // 
FIXME: waiting for API support
+                                               'ores_class' => $class,
+                                               'ores_probability' => 
$probability,
+                                               'ores_is_predicted' => ( 
$prediction === $class ),
+                                       );
+                               }
+                       }
+               }
+
+               wfGetDB( DB_MASTER )->insert( 'ores_classification', $dbData, 
__METHOD__ );
+               return true;
+       }
+}
diff --git a/includes/Hooks.php b/includes/Hooks.php
new file mode 100644
index 0000000..6e07e3f
--- /dev/null
+++ b/includes/Hooks.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace ORES;
+
+use ChangesListSpecialPage;
+use DatabaseUpdater;
+use EnhancedChangesList;
+use FormOptions;
+use Html;
+use JobQueueGroup;
+use OldChangesList;
+use RCCacheEntry;
+use RecentChange;
+
+class Hooks {
+       /**
+        * @param DatabaseUpdater $updater
+        */
+       public static function onLoadExtensionSchemaUpdates( DatabaseUpdater 
$updater ) {
+               $updater->addExtensionTable( 'ores_classification', __DIR__ . 
'/../ores.sql' );
+       }
+
+       public static function onRecentChange_save( RecentChange $rc ) {
+               if ( $rc->getAttribute( 'rc_type' ) === RC_EDIT ) {
+                       $job = new FetchScoreJob( $rc->getTitle(), array(
+                               'rcid' => $rc->getAttribute( 'rc_id' ),
+                               'revid' => $rc->getAttribute( 'rc_this_oldid' ),
+                       ) );
+                       JobQueueGroup::singleton()->push( $job );
+               }
+       }
+
+       /**
+        * @param ChangesListSpecialPage $clsp
+        * @param $filters
+        */
+       public static function onChangesListSpecialPageFilters( 
ChangesListSpecialPage $clsp, &$filters ) {
+               $filters['hidereverted'] = array(
+                       'msg' => 'ores-reverted-filter',
+                       'default' => false,
+               );
+       }
+
+       /**
+        * @param $name
+        * @param array $tables
+        * @param array $fields
+        * @param array $conds
+        * @param array $query_options
+        * @param array $join_conds
+        * @param FormOptions $opts
+        */
+       public static function onChangesListSpecialPageQuery(
+               $name, array &$tables, array &$fields, array &$conds,
+               array &$query_options, array &$join_conds, FormOptions $opts
+       ) {
+               if ( !$opts->getValue( 'hidereverted' ) ) {
+                       $tables[] = 'ores_classification';
+                       $fields[] = 'ores_probability';
+                       $join_conds['ores_classification'] = array( 'LEFT JOIN',
+                               'rc_this_oldid = ores_rev AND ores_model = 
\'reverted\' ' .
+                               'AND ores_is_predicted = 1 AND ores_class = 
\'true\'' );
+               }
+       }
+
+       /**
+        * @param EnhancedChangesList $ecl
+        * @param array $data
+        * @param RCCacheEntry[] $block
+        * @param RCCacheEntry $rcObj
+        */
+       public static function onEnhancedChangesListModifyLineData( 
EnhancedChangesList $ecl, array &$data,
+               array $block, RCCacheEntry $rcObj
+       ) {
+               $score = $rcObj->getAttribute( 'ores_probability' );
+               if ( $score !== null ) {
+                       $type = self::getRevertThreshold( $score );
+
+                       if ( $type ) {
+                               $data[] = self::getScoreHtml( $type, $score );
+
+                               $ecl->getOutput()->addModuleStyles( 
'ext.ores.styles' );
+                       }
+               }
+       }
+
+       // FIXME: Repeated code.
+       public static function onOldChangesListRecentChangesLine( 
OldChangesList &$ocl, &$html,
+               RecentChange $rc, array &$classes
+       ) {
+               $score = $rc->getAttribute( 'ores_probability' );
+               if ( $score !== null ) {
+                       $type = self::getRevertThreshold( $score );
+
+                       if ( $type ) {
+                               $html = $html . ' ' . self::getScoreHtml( 
$type, $score );
+
+                               $ocl->getOutput()->addModuleStyles( 
'ext.ores.styles' );
+                       }
+               }
+       }
+
+       protected static function getRevertThreshold( $score ) {
+               global $wgOresRevertTagThresholds;
+
+               $score = floatval( $score );
+               $type = null;
+               // TODO: Need to ensure the thresholds are ordered.
+               foreach ( $wgOresRevertTagThresholds as $name => $value ) {
+                       if ( $score >= $value ) {
+                               $type = $name;
+                       }
+               }
+               return $type;
+       }
+
+       protected static function getScoreHtml( $type, $score ) {
+               $cssClass = 'mw-ores-' . $type;
+               $rounded = intval( 100 * $score );
+               $msg = wfMessage( 'ores-reverted-' . $type )->numParams( 
$rounded )->escaped();
+               $html = Html::rawElement( 'span', array(
+                       'title' => $msg,
+                       'class' => array( $cssClass, 'mw-ores-score' ),
+               ), $msg );
+
+               return $html;
+       }
+}
diff --git a/modules/ext.ores.styles.css b/modules/ext.ores.styles.css
new file mode 100644
index 0000000..b7537f5
--- /dev/null
+++ b/modules/ext.ores.styles.css
@@ -0,0 +1,17 @@
+.mw-ores-high {
+       background: #f4908a;
+}
+
+.mw-ores-medium {
+       background: #ffbe99;
+}
+
+.mw-ores-low {
+       background: #ffe099;
+}
+
+.mw-ores-score {
+       margin-left: 1.5em;
+       padding: 0 1em 0 1em;
+       border-radius: .5em;
+}
diff --git a/ores.sql b/ores.sql
new file mode 100644
index 0000000..37bf880
--- /dev/null
+++ b/ores.sql
@@ -0,0 +1,23 @@
+-- ORES automated classifier outputs for a given revision
+--
+-- Each revision will usually be assigned a probability for all classes in the
+-- model's output range.
+CREATE TABLE /*_*/ores_classification (
+       -- Revision ID
+       ores_rev INTEGER(10) NOT NULL,
+       -- Model name
+       ores_model VARCHAR(32) NOT NULL,
+       -- Model version
+       ores_model_version VARCHAR(32) NOT NULL,
+       -- Classification title
+       ores_class VARCHAR(32) NOT NULL,
+       -- Estimated classification probability
+       ores_probability DECIMAL(10,10) NOT NULL,
+       -- Whether this classification has been recommended as the most likely
+       -- candidate.
+       ores_is_predicted TINYINT(1) NOT NULL
+) /*$wgDBTableOptions*/;
+
+CREATE INDEX /*i*/ores_rev ON /*_*/ores_classification (ores_rev);
+CREATE INDEX /*i*/ores_is_predicted ON /*_*/ores_classification 
(ores_is_predicted);
+CREATE INDEX /*i*/ores_winner ON /*_*/ores_classification (ores_rev, 
ores_is_predicted);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2717c89
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+  "scripts": {
+    "test": "grunt test"
+  },
+  "devDependencies": {
+    "grunt": "0.4.5",
+    "grunt-cli": "0.1.13",
+    "grunt-contrib-jshint": "0.11.3",
+    "grunt-banana-checker": "0.2.2",
+    "grunt-jscs": "2.1.0",
+    "grunt-jsonlint": "1.0.4"
+  }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..cb19440
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<ruleset>
+       <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki"/>
+       <file>.</file>
+       <arg name="extensions" value="php"/>
+       <exclude-pattern>vendor</exclude-pattern>
+</ruleset>

-- 
To view, visit https://gerrit.wikimedia.org/r/229423
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: If3b0f5357d57c8833a31e4e3e03f3284979df10d
Gerrit-PatchSet: 11
Gerrit-Project: mediawiki/extensions/ORES
Gerrit-Branch: master
Gerrit-Owner: Legoktm <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Halfak <[email protected]>
Gerrit-Reviewer: He7d3r <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Paladox <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Springle <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to