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