-
-
Save sunafterrainwm/e7159f9dc5648be1367aed3e48715304 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/* eslint-disable new-cap, no-underscore-dangle, camelcase */ | |
import jQuery = require( "jquery" ); | |
import { JSDOM } from "jsdom"; | |
import { ApiPage, ApiRevision, mwn, MwnOptions, MwnPage } from "mwn"; | |
import type { ApiQueryRevisionsParams } from "mwn/build/api_params"; | |
import { RecentChangeStreamEvent } from "mwn/build/eventstream"; | |
import { MwnError } from "mwn/build/error"; | |
import fs = require( "fs" ); | |
import path = require( "path" ); | |
import { execSync } from "child_process"; | |
import { URLSearchParams } from "url"; | |
console.log( "[\x1b[32mINFO\x1b[39m] Start process......" ); | |
const $ = jQuery( Object.assign( new JSDOM( "", { url: "https://zh.wikipedia.org/w/api.php" } ).window ), true ); | |
const mwnconfig: MwnOptions = { | |
apiUrl: "https://zh.wikipedia.org/w/api.php" | |
}; | |
const mwnbetaconfig: MwnOptions = { | |
apiUrl: "https://zh.wikipedia.beta.wmflabs.org/w/api.php" | |
}; | |
let mwbot: mwn; | |
let mwbetabot: mwn; | |
( async function () { | |
mwbot = await mwn.init( mwnconfig ); | |
mwbetabot = await mwn.init( mwnbetaconfig ); | |
mwbot.stream.recentchange( function ( event: RecentChangeStreamEvent ) { | |
if ( | |
event.wiki === "zhwiki" && | |
event.type === "categorize" && | |
event.title === "Category:正在等待審核的草稿" | |
) { | |
return true; | |
} | |
return false; | |
}, async function ( event: RecentChangeStreamEvent ) { | |
try { | |
const title: string = event.comment.replace( /^\[\[:?([^[\]]+)\]\].*$/, "$1" ); | |
console.log( event.comment, title ); | |
const html: string = await mwbot.parseTitle( title, { | |
uselang: "zh-hant" | |
} ); | |
const $parseHTML = $( $.parseHTML( html ) ); | |
const $submissionbox = $parseHTML.find( ".afc-submission-pending" ).length ? | |
$parseHTML.find( ".afc-submission-pending" ).first() : | |
$parseHTML.find( ".afc-submission" ).first(); | |
if ( !$submissionbox.length ) { | |
return; // publish / remove | |
} | |
const page = await AFCPage.new( title ); | |
page.cleanUp(); | |
if ( page.isChange ) { | |
const c = addCount(); | |
try { | |
const editdata = await page.postEdit(); | |
if ( !editdata.nochange ) { | |
console.log( `[\x1b[32mINFO\x1b[39m] #${c} comment: ${event.comment}, page: ${title}, tpage: ${editdata.title}, data: ${JSON.stringify( editdata )}, diff: ${editdata.newrevid}` ); | |
const message = `[DEBUG] #${c} 自動清理頁面<a href="https://zh.wikipedia.org/wiki/${encodeURI( title )}">${title}</a>,<a href="https://zh.wikipedia.beta.wmflabs.org/wiki/Special:Diff/${editdata.oldrevid}/${editdata.newrevid}">差異</a>。`; | |
const param = new URLSearchParams( { | |
chat_id: "-1001343771660", | |
text: message, | |
parse_mode: "HTML" | |
} ); | |
execSync( `curl "https://api.telegram.org/bot1906596141:AAHc9VchIY6ZdHEg5hEk87R7_I7mPEGT3OU/sendMessage?${param.toString()}"` ); | |
} else { | |
resetCount(); | |
} | |
} catch ( e ) { | |
console.error( "[\x1b[31mERROR\x1b[39m] Edit error: ", e ); | |
resetCount(); | |
} | |
} | |
} catch ( e ) { | |
console.error( "[\x1b[31mERROR\x1b[39m] Recentchange Error: (Throw by Async Function)", e ); | |
} | |
} ); | |
}() ); | |
const countPath = path.join( __dirname, "count.txt" ); | |
function addCount() { | |
try { | |
const txt = fs.readFileSync( countPath ).toString(); | |
let count = +txt; | |
if ( isNaN( count ) ) { | |
throw new Error( `Can't read "${countPath}".` ); | |
} else if ( count > 100 ) { | |
console.log( "[\x1b[32mINFO\x1b[39m] Process end." ); | |
process.exit( 0 ); | |
} else { | |
count++; | |
fs.writeFileSync( countPath, String( count ) ); | |
return count; | |
} | |
} catch ( e ) { | |
console.error( "[\x1b[31mERROR\x1b[39m]", e ); | |
process.exit( 1 ); | |
} | |
} | |
function resetCount() { | |
try { | |
const txt = fs.readFileSync( countPath ).toString(); | |
let count = +txt; | |
if ( isNaN( count ) ) { | |
throw new Error( `Can't read "${countPath}".` ); | |
} else { | |
count--; | |
fs.writeFileSync( countPath, String( count ) ); | |
} | |
} catch ( e ) { | |
console.error( "[\x1b[31mERROR\x1b[39m]", e ); | |
process.exit( 1 ); | |
} | |
} | |
// const summary = "[[Wikipedia:机器人/申请/Sunny00217Bot|任務1]]:自動清理AFC草稿(測試中)"; | |
const summary = "BOT: 自動清理AFC草稿"; | |
const tmpprefix = "User:Sunny00217Bot/afctest/rev/"; | |
async function revisionRequest( title: string, props: ApiQueryRevisionsParams["rvprop"], | |
limit: number, customOptions?: ApiQueryRevisionsParams ): Promise<ApiRevision[]> { | |
const data = await mwbot.request( { | |
action: "query", | |
prop: "revisions", | |
titles: title, | |
rvprop: props || "ids|timestamp|flags|comment|user", | |
rvlimit: limit || 50, | |
rvslots: "main", | |
...customOptions | |
} ); | |
const page: ApiPage = data?.query?.pages[ 0 ]; | |
if ( !page || page.missing ) { | |
throw new MwnError.MissingPage(); | |
} | |
return page.revisions || []; | |
} | |
type ApiEditResponse = { | |
result: string; | |
pageid: number; | |
title: string; | |
contentmodel: string; | |
nochange?: boolean; | |
oldrevid: number; | |
newrevid: number; | |
newtimestamp: string; | |
}; | |
class AFCPage { | |
private _page: MwnPage; | |
get mwnpage(): MwnPage { | |
return this._page; | |
} | |
private _oldtext!: string; | |
private _text!: string; | |
// private _starttimestamp: string = new Date().toISOString(); | |
// private _basetimestamp!: string; | |
private _baserevid!: number; | |
private _templates: { | |
target: string; | |
params: Record<string, string>; | |
}[] = []; | |
private _submimissions: { | |
status: string; | |
timestamp: string | null; | |
params: Record<string, string>; | |
}[] = []; | |
// Holds all comments on the page | |
private _comments: { | |
timestamp: string | null; | |
text: string; | |
}[] = []; | |
private categories: Record<string, string | false> = {}; | |
private constructor( title: string ) { | |
this._page = new mwbot.page( title ); | |
} | |
private async _init(): Promise<void> { | |
this._page.history = function ( props: ApiQueryRevisionsParams["rvprop"], | |
limit: number, customOptions?: ApiQueryRevisionsParams ) { | |
return revisionRequest( this.toString(), props, limit, customOptions ); | |
}; | |
await this._getPageText(); | |
await this._getTemplates(); | |
} | |
public static async new( title: string ): Promise<AFCPage> { | |
const page = new AFCPage( title ); | |
await page._init(); | |
await mwbetabot.save( | |
tmpprefix + page._baserevid, | |
page._oldtext, | |
"BOT: COPY FROM https://zh.wikipedia.org/wiki/Special:Redirect/revision/" + page._baserevid | |
); | |
return page; | |
} | |
private async _getPageText(): Promise<void> { | |
const history = await this._page.history( [ "content", "ids" ], 1 ); | |
const rev = history[ 0 ]; | |
this._oldtext = this._text = rev.slots.main.content; | |
// this._basetimestamp = rev.timestamp; | |
this._baserevid = rev.revid; | |
} | |
private async _getTemplates(): Promise<void> { | |
if ( !this._text ) { | |
throw new ReferenceError( "You must call method _getPageText before call method _getTemplates !" ); | |
} | |
const templates: { | |
target: string; | |
params: Record<string, string>; | |
}[] = this._templates = []; | |
const title = this._page.toString(); | |
/** | |
* Essentially, this function takes a template value DOM object, $v, | |
* and removes all signs of XML-ishness. It does this by manipulating | |
* the raw text and doing a few choice string replacements to change | |
* the templates to use wikicode syntax instead. Rather than messing | |
* with recursion and all that mess, /g is our friend...which is | |
* pefectly satisfactory for our purposes. | |
* | |
* @param {JQuery} $v | |
* @return {string} | |
*/ | |
function parseValue( $v: JQuery ): string { | |
let text: string = $( "<div>" ).append( $v.clone() ).html(); | |
// Convert templates to look more template-y | |
text = text.replace( /<template([^>]+)?>/g, "{{" ); | |
text = text.replace( /<\/template>/g, "}}" ); | |
text = text.replace( /<part>/g, "|" ); | |
// Expand embedded tags (like <nowiki>) | |
text = text.replace( | |
new RegExp( | |
"<ext><name>(.*?)<\\/name>(?:<attr>.*?<\\/attr>)*" + | |
"<inner>(.*?)<\\/inner><close>(.*?)<\\/close><\\/ext>", | |
"g" ), | |
"<$1>$2$3" ); | |
// Now convert it back to text, removing all the rest of the XML | |
// tags | |
try { | |
return $( text ).text(); | |
} catch ( e ) { | |
return $( $.parseHTML( text ) ).text(); | |
} | |
} | |
const { expandtemplates }: { | |
expandtemplates: { | |
parsetree: string; | |
}; | |
} = Object.assign( await mwbot.request( { | |
action: "expandtemplates", | |
title: title, | |
text: this._text, | |
prop: "parsetree" | |
} ) ); | |
const $templateDom = $( $.parseXML( expandtemplates.parsetree ) ).find( "root" ); | |
$templateDom.children( "template" ).each( function () { | |
const $el = $( this ), | |
data: typeof AFCPage.prototype._templates[0] = { target: $el.children( "title" ).text(), params: {} }; | |
$el.children( "part" ).each( function () { | |
const $part = $( this ); | |
const $name = $part.children( "name" ); | |
// Use the name if set, or fall back to index if implicitly | |
// numbered | |
const name = ( $name.text() || $name.attr( "index" ) ).trim(); | |
const value = ( parseValue( $part.children( "value" ) ) ).trim(); | |
data.params[ name ] = value; | |
} ); | |
templates.push( data ); | |
} ); | |
this._parseSubmissionTemplate(); | |
} | |
private _parseSubmissionTemplate(): void { | |
function getAndDelete<O, V extends keyof O>( obj: O, key: V ): O[V] { | |
if ( Object.prototype.hasOwnProperty.call( obj, key ) ) { | |
const value = obj[ key ]; | |
delete obj[ key ]; | |
return value; | |
} | |
return undefined; | |
} | |
this._submimissions = []; | |
this._comments = []; | |
function parseTimestamp( str: string ) { | |
let exp: RegExp; | |
if ( !isNaN( +new Date( str ) ) ) { | |
return new Date( str ).toISOString().replace( /[^\d]/g, "" ).slice( 0, 14 ); | |
} else if ( str.match( /^\d+$/ ) ) { | |
return str; | |
} else { | |
exp = /^(\d{4})年(\d{1,2})月(\d{1,2})日 \([一二三四五六日]\) (\d\d):(\d\d) \(UTC\)$/g; | |
} | |
const match = exp.exec( str ); | |
if ( !match ) { | |
return false; | |
} | |
const date = new Date(); | |
date.setUTCFullYear( +match[ 1 ] ); | |
date.setUTCMonth( +match[ 2 ] - 1 ); // stupid javascript | |
date.setUTCDate( +match[ 3 ] ); | |
date.setUTCHours( +match[ 4 ] ); | |
date.setUTCMinutes( +match[ 5 ] ); | |
if ( +match[ 6 ] ) { | |
date.setMilliseconds( +match[ 6 ] ); | |
} | |
date.setUTCSeconds( 0 ); | |
return date.toISOString().replace( /[^\d]/g, "" ).slice( 0, 14 ); | |
} | |
for ( const template of this._templates ) { | |
const name = template.target.toLowerCase(); | |
if ( name === "afc submission" ) { | |
this._submimissions.push( { | |
status: ( getAndDelete( template.params, "1" ) || "" ).toLowerCase(), | |
timestamp: parseTimestamp( getAndDelete( template.params, "ts" ) ) || null, | |
params: template.params | |
} ); | |
} else if ( name === "afc comment" ) { | |
this._comments.push( { | |
timestamp: parseTimestamp( template.params[ "1" ] ) || null, | |
text: template.params[ "1" ] | |
} ); | |
} | |
} | |
type timestampSortObj = | |
typeof AFCPage.prototype._submimissions[0] | typeof AFCPage.prototype._comments[0]; | |
function timestampSortHelper( a: timestampSortObj, b: timestampSortObj ) { | |
// If we're passed something that's not a number -- | |
// for example, {{REVISIONTIMESTAMP}} -- just sort it | |
// first and be done with it. | |
if ( isNaN( +a.timestamp ) ) { | |
return -1; | |
} else if ( isNaN( +b.timestamp ) ) { | |
return 1; | |
} | |
// Otherwise just sort normally | |
return +b.timestamp - +a.timestamp; | |
} | |
// Sort templates by timestamp; most recent are first | |
this._submimissions.sort( timestampSortHelper ); | |
this._comments.sort( timestampSortHelper ); | |
let isPending = false; | |
let isUnderReview = false; | |
let isDeclined = false; | |
let isDraft = false; | |
// Useful list of "what to do" in each situation. | |
const statusCases = { | |
// Declined | |
d: function () { | |
if ( !isPending && !isDraft && !isUnderReview ) { | |
isDeclined = true; | |
} | |
return true; | |
}, | |
// Draft | |
t: function () { | |
// If it's been submitted or declined, remove draft tag | |
if ( isPending || isDeclined || isUnderReview ) { | |
return false; | |
} | |
isDraft = true; | |
return true; | |
}, | |
// Under review | |
r: function () { | |
if ( !isPending && !isDeclined ) { | |
isUnderReview = true; | |
} | |
return true; | |
}, | |
// Pending | |
"": function () { | |
// Remove duplicate pending templates or a redundant | |
// pending template when the submission has already been | |
// declined / is already under review | |
if ( isPending || isDeclined || isUnderReview ) { | |
return false; | |
} | |
isPending = true; | |
isDraft = false; | |
isUnderReview = false; | |
return true; | |
} | |
}; | |
// Process the submission templates in order, from the most recent to | |
// the oldest. In the process, we remove unneeded templates (for example, | |
// a draft tag when it's already been submitted) and also set various | |
// "isX" properties of the Submission. | |
this._submimissions = this._submimissions.filter( function ( template ) { | |
let keepTemplate = true; | |
if ( Object.prototype.hasOwnProperty.call( statusCases, template.status ) ) { | |
keepTemplate = statusCases[ <"d" | "t" | "r" | ""><unknown>template.status ](); | |
} else { | |
// Default pending status | |
keepTemplate = statusCases[ "" ](); | |
} | |
if ( keepTemplate ) { | |
// Will be re-added in updateAfcTemplates() if necessary | |
delete template.params.small; // small=yes for old declines | |
} | |
return keepTemplate; | |
} ); | |
} | |
private _removeExcessNewlines(): void { | |
this._text = this._text | |
// Replace 4+ newlines with just three | |
.replace( /([\s\t]*[\r\n]){3,}/g, "\n\n\n" ) | |
// Remove initial request artifact | |
.replace( /=+([^=\n]+)=+[\t\n\s]*$/gi, function ( _all: string, title: string ) { | |
const regexp = /^(?:外部(?:[链鏈]接|[连連]結)|[参參]考(?:[资資]料|[来來]源|文[档檔献獻]))$/; | |
return regexp.exec( title ) ? `== ${title} ==` : ""; | |
} ) | |
.trim(); | |
} | |
private _removeAfcTemplates(): void { | |
// FIXME: Awful regex to remove the old submission templates | |
// This is bad. It works for most cases but has a hellish time | |
// with some double nested templates or faux nested templates (for | |
// example "{{hi|{ foo}}" -- note the extra bracket). Ideally Parsoid | |
// would just return the raw template text as well (currently | |
// working on a patch for that, actually). | |
this._text = this._text.replace( | |
new RegExp( | |
"\\{\\{\\s*afc submission\\s*(?:\\||[^{{}}]*|{{.*?}})*?\\}\\}" + | |
// Also remove the AFCH-generated warning message, since if | |
// necessary the script will add it again | |
"(<!-- 请不要移除这一行代码 -->)?", | |
"gi" ), | |
"" ); | |
this._text = this._text.replace( /\{\{\s*afc comment[\s\S]+?\(UTC\)\}\}/gi, "" ); | |
// Remove horizontal rules that were added by AFCH after the comments | |
this._text = this._text.replace( /^----+$/gm, "" ); | |
// Remove excess newlines created by AFC templates | |
this._removeExcessNewlines(); | |
} | |
private _updateAfcTemplates(): void { | |
this._removeAfcTemplates(); | |
const templates: string[] = []; | |
let hasDeclineTemplate = false; | |
// Submission templates go first | |
this._submimissions.forEach( function ( template ) { | |
let tout = "{{AFC submission|" + template.status; | |
const paramKeys: string[] = []; | |
// FIXME: Think about if we really want this elaborate-ish | |
// positional parameter ouput, or if it would be a better | |
// idea to just make everything absolute. When we get to a point | |
// where nobody is using the actual templates and it's 100% | |
// script-based, "pretty" isn't really that important and we | |
// can scrap this. Until then, though, we can only dream... | |
// Make an array of the parameters | |
Object.keys( template.params ).forEach( function ( key ) { | |
// Parameters set to false are ignored | |
if ( template.params[ key ] ) { | |
paramKeys.push( key ); | |
} | |
} ); | |
paramKeys.sort( function ( a, b ) { | |
const aIsNumber = !isNaN( +a ), bIsNumber = !isNaN( +b ); | |
// If we're passed two numerical parameters then | |
// sort them in order (1,2,3) | |
if ( aIsNumber && bIsNumber ) { | |
return ( +a ) > ( +b ) ? 1 : -1; | |
} | |
// A is a number, it goes first | |
if ( aIsNumber && !bIsNumber ) { | |
return -1; | |
} | |
// B is a number, it goes first | |
if ( !aIsNumber && bIsNumber ) { | |
return 1; | |
} | |
// Otherwise just leave the positions as they were | |
return 0; | |
} ).forEach( function ( key: string, index: number ) { | |
const value = template.params[ key ]; | |
if ( | |
// If it is a numerical parameter, doesn't include | |
// `=` in the value, AND is in sequence with the other | |
// numerical parameters, we can omit the key= part | |
// (positional parameters, joyous day :/ ) | |
!isNaN( +key ) && +key % 1 === 0 && value.indexOf( "=" ) === -1 && | |
// Parameter 2 will be the first positional parameter, | |
// since 1 is always going to be the submission status. | |
( key === "2" || +paramKeys[ index - 1 ] === +key - 1 ) | |
) { | |
tout += "|" + value.trim(); | |
} else { | |
tout += "|" + key + "=" + value; | |
} | |
} ); | |
// Collapse old decline template if a newer decline | |
// template is already displayed on the page | |
if ( hasDeclineTemplate && template.status === "d" ) { | |
tout += "|small=yes"; | |
} | |
// So that subsequent decline templates will be collapsed | |
if ( template.status === "d" ) { | |
hasDeclineTemplate = true; | |
} | |
// Finally, add the timestamp and a warning about removing the template | |
tout += "|ts=" + template.timestamp + "}}"; | |
templates.push( tout ); | |
} ); | |
// Then comment templates | |
this._comments.forEach( function ( comment ) { | |
templates.push( "\n{{AFC comment|1=" + comment.text + "}}" ); | |
} ); | |
// If there were comments, add a horizontal rule beneath them | |
if ( this._comments.length ) { | |
templates.push( "\n----" ); | |
} | |
this._text = templates.join( "\n" ) + "\n\n" + this._text; | |
} | |
private _parseCategories(): void { | |
const categoryRegex = /\[\[:?(?:[Cc]at|CAT|[Cc]ategory|CATEGORY|分[类類]):([^[\]]+)\]\]/gi; | |
let match = categoryRegex.exec( this._text ); | |
this.categories = {}; | |
while ( match ) { | |
const split = match[ 1 ].split( "|" ); | |
if ( !Object.prototype.hasOwnProperty.call( this.categories, split[ 0 ] ) ) { | |
this.categories[ split.shift() ] = split.length ? split.join( "|" ) : false; | |
} | |
match = categoryRegex.exec( this._text ); | |
} | |
this._text = this._text.replace( /\[\[:?(?:Cat|Category|分[类類]):([^[\]]+)\]\]/gi, "" ); | |
this._removeExcessNewlines(); | |
} | |
public updateCategories( categories?: string[] ): void { | |
this._parseCategories(); | |
if ( categories ) { | |
for ( const cat of categories ) { | |
const split = cat.split( "|" ); | |
if ( !Object.prototype.hasOwnProperty.call( this.categories, split[ 0 ] ) ) { | |
this.categories[ split.shift() ] = split.length ? split.join( "|" ) : false; | |
} | |
} | |
} | |
const CategoriesNeedRemove = [ "使用创建条目精灵建立的页面", "用条目向导创建的草稿", "正在等待審核的草稿" ]; | |
for ( const cat in this.categories ) { | |
if ( CategoriesNeedRemove.indexOf( cat ) === -1 ) { | |
this._text += `\n[[:Category:${cat}${( this.categories[ cat ] ? `|${this.categories[ cat ]}` : "" )}]]`; | |
} | |
} | |
} | |
public cleanUp(): void { | |
let text = this._text; | |
const commentsToRemove = [ | |
"请不要移除这一行代码", "請勿刪除此行,並由下一行開始編輯" | |
]; | |
// Assemble a master regexp and remove all now-unneeded comments | |
// (commentsToRemove) | |
const commentRegex = new RegExp( | |
"<!-{2,}\\s*(" + commentsToRemove.join( "|" ) + ")\\s*-{2,}>", "gi" ); | |
text = text | |
.replace( commentRegex, "" ) | |
// Remove sandbox templates | |
.replace( | |
/\{\{(userspacedraft|userspace draft|user sandbox|用戶沙盒|用户沙盒|draft copyvio|七日草稿|7D draft|Draft|草稿|Please leave this line alone \(sandbox heading\))(?:\{\{[^{}]*\}\}|[^}{])*\}\}/ig, | |
"" | |
) | |
// 把分類當成模板來加? | |
.replace( /\{\{:?(?:Cat|Category|分[类類]):([^{}]+)\}\}/gi, "[[:Category:$1]]" ) | |
// 移除掉不知道為甚麼沒移除的預設內容 | |
.replace( /'''此处改为条目主题'''(?:是一个)?/, "" ) | |
.replace( /==\s*章节标题\s*==/, "" ) | |
.replace( /<([A-Za-z]+)(\s+([^>]+))?>/g, function ( _all: string, tagName: string, args: string ) { | |
return `<${tagName.toLowerCase()}${args ? ` ${args.trim()}` : ""}>`; | |
} ) | |
.replace( /<\/([A-Za-z]+)(?:\s+[^>]+)?>/g, function ( _all: string, tagName: string ) { | |
return `</${tagName.toLowerCase()}>`; | |
} ) | |
.replace( /<\/?br\s*\/?>/g, "<br />" ) | |
.replace( /<\/?hr\s*\/?>/g, "<hr />" ) | |
// Remove html comments (<!--) that surround categories | |
.replace( /<!--\s*((?:\[\[:{0,1}(?:[Cc]at|CAT|[Cc]ategory|CATEGORY|分[类類]):.*?\]\]\s*)+)-->/gi, "$1" ) | |
// Remove html comments (<!--) that surround categories | |
.replace( /<!--\s*((?:\[\[:{0,1}(?:[Cc]at|CAT|[Cc]ategory|CATEGORY|分[类類]):.*?\]\]\s*)+)-->/gi, "$1" ) | |
// Remove spaces/commas between <ref> tags | |
.replace( | |
/\s*(<\/\s*ref\s*>)\s*[,]*\s*(<\s*ref\s*(name\s*=|group\s*=)*\s*[^/]*>)[ \t]*(\n|$)/gm, | |
"$1$2\n" | |
) | |
// Remove whitespace before <ref> tags | |
.replace( /[\s\t]*(<\s*ref\s*(name\s*=|group\s*=)*\s*.*[^/]+>)[\s\t]*(\n|$)/gm, "$1\n\n" ) | |
// Move punctuation before <ref> tags | |
.replace( | |
/\s*((<\s*ref\s*(name\s*=|group\s*=)*\s*.*[/]{1}>)|(<\s*ref\s*(name\s*=|group\s*=)*\s*[^/]*>(?:<[^<>\n]*>|[^<>\n])*<\/\s*ref\s*>))[\s\t]*([.。!!??,,;;::])+(\n|$)/gm, | |
"$6$1\n\n" | |
) | |
// Replace {{http://example.com/foo}} with "* http://example.com/foo" (common | |
// newbie error) | |
.replace( | |
/\n\{\{(http[s]?|ftp[s]?|irc):\/\/(.*?)\}\}/gi, | |
"\n* $1://$3" | |
); | |
// Convert http://-style links to other wikipages to wikicode syntax | |
// FIXME: Break this out into its own core function? Will it be used | |
// elsewhere? | |
function convertExternalLinksToWikilinks( text: string ) { | |
const linkRegex = | |
/\[{1,2}(?:https?:)?\/\/(?:(?:zh\.wikipedia\.org|zhwp\.org)\/(?:wiki|zh|zh-hans|zh-hant|zh-cn|zh-my|zh-sg|zh-tw|zh-hk|zh-mo)|zhwp\.org)\/([^\s|\][]+)(?:\s|\|)?((?:\[\[[^[\]]*\]\]|[^\][])*)\]{1,2}/ig; | |
let linkMatch = linkRegex.exec( text ); | |
let title: string; | |
let displayTitle: string; | |
let newLink: string; | |
while ( linkMatch ) { | |
title = decodeURI( linkMatch[ 1 ] ).replace( /_/g, " " ); | |
displayTitle = decodeURI( linkMatch[ 2 ] ).replace( /_/g, " " ); | |
// Don't include the displayTitle if it is equal to the title | |
if ( displayTitle && title !== displayTitle ) { | |
newLink = "[[" + title + "|" + displayTitle + "]]"; | |
} else { | |
newLink = "[[" + title + "]]"; | |
} | |
text = text.replace( linkMatch[ 0 ], newLink ); | |
linkMatch = linkRegex.exec( text ); | |
} | |
return text; | |
} | |
text = convertExternalLinksToWikilinks( text ); | |
this._text = text; | |
this.updateCategories(); | |
this._removeExcessNewlines(); | |
this._updateAfcTemplates(); | |
} | |
public get hasVaildAFCTemplates(): boolean { | |
return !!this._submimissions.length; | |
} | |
public get text(): string { | |
return this._text; | |
} | |
public set text( str: string ) { | |
this._text = str; | |
} | |
public get isChange(): boolean { | |
return this._text !== this._oldtext; | |
} | |
public get isAFCPage(): boolean { | |
return [ 2, 118 ].includes( this._page.namespace ); | |
/* || this._page.namespace === 102 && !!this._page.getMainText().match( /^建立條目\// ) */ | |
} | |
public postEdit(): Promise<ApiEditResponse> { | |
/* | |
if ( !mwbot.loggedIn && !mwbot.usingOAuth ) { | |
throw new Error( "You must login to edit." ); | |
} | |
return this._page.save( this.text, summary, { | |
bot: true, | |
starttimestamp: this._starttimestamp, | |
basetimestamp: this._basetimestamp, | |
baserevid: this._baserevid, | |
minor: true, | |
nocreate: true | |
} ); | |
*/ | |
if ( !mwbetabot.loggedIn && !mwbetabot.usingOAuth ) { | |
throw new Error( "You must login to edit." ); | |
} | |
// eslint-disable-next-line @typescript-eslint/no-this-alias | |
const page = this; | |
return mwbetabot.save( | |
tmpprefix + page._baserevid, | |
page.text, | |
summary, | |
{ | |
minor: true, | |
nocreate: true | |
} | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment