User:Disgustedorite/SaganBot.js
From Sagan 4 Alpha Wiki
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/** JSaganBot version Attercop written by [[User:Disgustedorite]]
* Special thanks to Joeytje50's JS Wiki Browser and AnguaNatalia's Sagan 4 Wiki Parser, which I referenced / will refrence when making this.
*
* Please keep in mind that while I am attempting to replicate some of the functions of AN's code,
* hers was written in php and was designed to speed up wiki editing rather than automate it,
* and likewise this is not and could never be an exact replica of the original Sagan 4 Wiki Parser.
* Therefore, please direct any issues you have using this script to me, not to her.
*/
//Custom commands start here and may be used from the console anywhere
const api = new mw.Api();
const nonBiomes = [
"MICRO",
"MACRO",
"Micro",
"Macro",
"Microscopic",
"Macroscopic",
"Water Tables",
"Mixed Scrub",
"Twilight Zone",
"Midnight Zone",
"Larvae",
"Adult",
"Adults",
"Larva",
"Spores",
"Seeds",
"Eggs"
];//these, which may be found mid-biome, are explicitly not considered biomes and should be skipped.
const canontheticals = [
"Sagan 4",
"Mason",
"Photic",
"Nonphotic",
"Troposphere",
"Stratosphere",
"Global",
"Sunlight Zone",
"Twilight Zone",
"Midnight Zone",
"Abyssal Zone"
];//canon parentheticals to be ignored while parsing biomes
const JSaganBot = {
getCompendium(id) {
//returns html contents of https://forum.sagan4.org/index.php?act=Print&client=printer&t=1470 (replacing 1470 with the specified ID)
//currently non-functional for security reasons
fetch("https://forum.sagan4.org/index.php?act=Print&client=printer&t="+id)
.then(response => {
if (!response.ok) {
console.error("Could not obtain compendium");
}
return response.text();
});
},
convertEcoPage(grouplink) {
var topreturn = "";
if (grouplink) {
topreturn = "|"+grouplink;
}
let isEditor = document.getElementById("wpPreview");
var modifiedcontent = "";
if (isEditor == null) {
console.error("You must have the editor open to use this.");
} else {
//console.log("This page has an editor. However, the function is not yet...functional. hehe");
var pageContents = document.getElementById("wpTextbox1").value;
var pageLines = pageContents.split("\n");
var currentBiome = null;
var insideList = false;
pageLines.forEach((line,index) => {
if (currentBiome == null) {
//This will only be the case at the beginning of the article. Basically, this exists to skip until it gets to a biome's subheading. I may want to do something here later
} else {
//detect if the line is a species, and act accordingly
if (line.match(/^'*\[\[[^\:\|\(\)\[\]\=]+\]\]\s*\(*[^\|\(\)\[\]\=]*\)*'*.*$/)) {
//do expected calculations for species here
let speciesLink = line.match(/\[\[([^\(\)\[\]\=]+)\]\]/)[1];
let parenthetical = "";
if (line.match(/\]\]\s*\(*([^\|\(\)\[\]\=]+)*\)/) != null) {
parenthetical = " ("+line.match(/\]\]\s*\(*([^\|\(\)\[\]\=]+)*\)/)[1]+")";
}
line = "|"+speciesLink+parenthetical;
if (insideList == false) {
//begin list
line = "{{#invoke:EcoData|main|biome="+currentBiome+"\n\n"+line;
insideList = true;
}
} else if (line.match(/^\s*$/) == null && insideList == true) {
console.log("Not a species line! Contents: "+line);
line = "}}{{topreturn"+topreturn+"}}\n\n"+line;
insideList = false;
}
}
if (line.match(/[\=\[][^\|\[]*[\|\[\=]+([^\|\[\]]+)\]*\=+/) != null && nonBiomes.includes(line.match(/[\|\[\=]+([^\|\[\]\=]+)\]*\=+/)[1]) == false){
//Valid biome detected! It skips anything with one word except groups. I could write an exception, but biomes like Reef only existed, like, very briefly very early on, so...
//if (line.match("|")) {
// currentBiome = line.match(/\=\=\[\[[\w\s\-\d\(\)]\|([^\]]+])/)[1];
//} else {
//
//}
currentBiome = line.match(/[\|\[\=]+([^\|\[\]\=]+)\]*\=+/)[1];
console.log("Biome detected! It is called: "+currentBiome);
}
modifiedcontent = modifiedcontent + "\n" + line;
});
if (insideList == true) {
//ends the last list if it's still open
modifiedcontent = modifiedcontent + "}}{{topreturn"+topreturn+"}}";
insideList == false;
}
if (modifiedcontent != "") {
document.getElementById("wpTextbox1").value = modifiedcontent;
console.log('Conversion complete, but I might have made mistakes. Please double check my work with the "show changes" button!');
} else {
console.error("Failed to convert page. Please refresh the page to undo my work.");
}
}
},
addToEcoPage(species,biomes,week) {
console.warn("The automatic ecosystem page updater has not been implemented yet!");
console.log("When it is, though, it will take a species and put it exactly where it goes on the ecosystem page.");
//No params: runs through entire current generation
//Species param: Only does the single species
//Biomes param: A string containing a comma-separated list bypasses the species' own biome list
//Week param: add to a specific period's ecosystem page
var errorstatus = false;
var speciesList;
var inputGen = 167;
var thisWeek = 27;
var isSpeciesGotten = false;
var isEcoPageGotten = false;
var subpageList;
if (week) {
thisWeek = number(week);
}
if (species && typeof species !== "number") {
speciesList = species.split(", ");
console.log("Species list: ",speciesList);
isSpeciesGotten = true;
} else {
//get a list of species from the current generation page
if (species && isNaN(number(species)) == false) {
inputGen = number(species);
}
}
const ecoPromise = new Promise((resolve, reject) => {api.get({
action: "query",
format: "json",
list: "prefixsearch",
pslimit: 20,
pssearch: "Week "+thisWeek+"/Ecosystem/" //normal use
//pssearch: "User:Disgustedorite/sandbox/AutoEcoTest/" //testing override
}).fail(function(error) {
console.error("Failed to get ecosystem subpages: ",error);
errorstatus = true;
reject("Failure");
}).done(function(data) {
var ecoSubpages = data.query.prefixsearch;
if (ecoSubpages) {
subpageList = ecoSubpages.map(obj => obj.title);
console.log("Ecosystem subpages detected! They are: ",subpageList);
} else {
console.log("Ecosystem page had no subpages. Now counting itself as the sole subpage instead.");
subpageList = [ "Week "+thisWeek+"/Ecosystem" ];
}
isEcoPageGotten = true;
resolve("Success");
});
});
if (species && typeof species !== "number") {
speciesList = species.split(", ");
console.log("Species list: ",speciesList);
isSpeciesGotten = true;
} else {
//get a list of species from the current generation page
if (species && isNaN(number(species)) == false) {
inputGen = number(species);
}
}
const speciesPromise = new Promise((resolve, reject) => {api.get({
action: 'query',
titles: "Generation "+inputGen,
prop: 'revisions',
rvprop: 'content',
format: "json",
}).fail(function(error) {
if (isSpeciesGotten == false) {
console.error("Could not obtain generation page: ", error);
errorstatus = true;
reject("Failure");
} else {
resolve("Success");
}
}).done(function(data) {
if (isSpeciesGotten == false) {
console.log("Successfully obtained generation page.");
let pageId = Object.keys(data.query.pages)[0];
let stringData = data.query.pages[pageId].revisions[0]['*'];
//console.log("Page parsed! Its contents are: ",stringData);
//get a list of species based on contents of the generation page
speciesList = stringData.match(/\.\w*\|\[\[([^\|\:\[\]]*)\]\]/g); //makes an array of every species listed on the generation page
speciesList = speciesList.map(speciesLine => speciesLine.replace(/\.\w*\|\[\[(.*?)\]\]/g, '$1'));
console.log("Species list: ",speciesList);
isSpeciesGotten = true;
}
resolve("Success");
});
});
Promise.all([ecoPromise,speciesPromise]).then(([ecoResult, speciesResult]) => {
if (errorstatus == true) {
console.error("Could not complete task: ecosystem subpages, species list, or both could not be obtained");
} else {
console.log("Species list and ecosystem subpages both successfully obtained");
//Get ecosystem subpage content
const subpageContentPromises = [];
var subpageStatuses = {};
var subpageContents = {};
subpageList.forEach(pagename => {
subpageContentPromises.push(new Promise((resolve, reject) => {api.get({
action: 'query',
titles: pagename,
prop: 'revisions',
rvprop: 'content',
format: "json",
}).fail(function(error) {
subpageStatuses[pagename] = false;
reject(pagename +" failed");
}).done(function(data) {
subpageStatuses[pagename] = true;
let pageId = Object.keys(data.query.pages)[0];
let stringData = data.query.pages[pageId].revisions[0]['*'];
subpageContents[pagename] = stringData.split("\n");
resolve(pagename + " succeeded");
});
}));
});
Promise.all(subpageContentPromises)
.then(results => {
console.log("Finished getting ecosystem page content: ",subpageStatuses);
//now take each species, get its size and biomes, and get to work!!
var speciesPages = {};
var speciesDataPromises = [];
speciesList.forEach((species,index) => {
//get species content, the promise is to evade collision (these should not be asynchronous!)
let speciesPromise = new Promise((resolve, reject) => {api.get({
action: 'query',
titles: species,
prop: 'revisions',
rvprop: 'content',
format: "json",
}).fail(function(error) {
//console.error("Failed to get page content for species ",species);
reject("Failure");
}).done(function(data) {
let pageId = Object.keys(data.query.pages)[0];
//console.log(pageId);
if (pageId != -1) {
let stringData = data.query.pages[pageId].revisions[0]['*'];
var speciesText = stringData;
speciesPages[species] = speciesText;
resolve("Success");
} else {
//console.error("Failed to get page content for species ",species);
reject("Failure");
}
});});
speciesDataPromises.push(speciesPromise);
speciesPromise.then((result) => {
//console.log("Getting species data for "+species+" was a ",result);
}).catch((result => {
console.error("Getting species data for "+species+" was a failure.");
}));
});
Promise.allSettled(speciesDataPromises).then(results => {
console.log("Species and their pages are as follows: ",speciesPages);
//NOW we get this show on a road
for (const [species, page] of Object.entries(speciesPages)) {
if (page.match(/\{\{radiation|RemoteSpecies/)) {
console.warn("Compound entries are not supported. You will need to add "+species+" to the ecosystem page manually.");
continue; //prevents further execution for the compound entry
}
let habitatList = page.match(/\|habitat\s*\=\s*([^\=\n\|\}]*)\n+[\|\}]/)[1];
let size = page.match(/\|size\s*\=\s*([^\=\n\|\}]*)\n+[\|\}]/)[1]; //get size to determine 1) if it's micro or macro 2) what genus size class it goes under
if (/\|\s*habitat2\s*[^\s\n\|\}]/.test(habitatList) == true) {
console.warn(species+" is using the deprecated habitat2 parameter! Please convert it to the modern format, as some of its biomes will be missed.");
}
let micromacro;
let sizeclass;
if (size.match(/\scm|\sm(\;|\s|$)|\skm|\scentimeter|\smeter|\skilometer/)) {
micromacro = "macro";
} else {
micromacro = "micro";
}
switch (true) {
case /(^|\s|\-)([2-9][1-9]|[3-9]\d)(\.\d+)?\s(cm|centimeters?)|\d\d\d\s(cm|centimeters?)|\d\s(m|meters?)(\;|\s|$)/i.test(size):
sizeclass = ">20 centimeters";
break;
case /(^|\s|\-)\d\d(\.\d+)?\s(cm|centimeters?)/i.test(size):
sizeclass = "10+ centimeters";
break;
case /(^|\s|\-)\d(\.\d+)?\s(cm|centimeters?)/i.test(size):
sizeclass = "1+ centimeters";
break;
case /(^|\s|\-)\d(\.\d+)?\s(mm|millimeters?)/i.test(size):
sizeclass = "1+ millimeters";
break;
case /(^|\s|\-)\d\d\d(\.\d+)?\s(\xb5m|micrometers?|um)/i.test(size):
sizeclass = "100+ micrometers";
break;
case /(^|\s|\-)\d\d(\.\d+)?\s(\xb5m|micrometers?|um)/i.test(size):
sizeclass = "10+ micrometers";
break;
case /(^|\s|\-)[1-9](\.\d+)?\s(\xb5m|micrometers?|um)/i.test(size):
sizeclass = "1+ micrometers";
break;
case /(^|\s|\-)0\.\d\s(\xb5m|micrometers?|um)/i.test(size):
sizeclass = "<1 micrometer";
break;
default:
sizeclass = "unknown";
}
//console.log(species+" is considered "+micromacro+"scopic and is of the size class "+sizeclass+".\nIts size value was read as: "+size);
//parse biome list
if (habitatList.match(/^.*:/) == null) {
//console.log(species+" had a habitat list which did not start with a category. It was: "+habitatList);
habitatList = "Placeholder: "+habitatList;
}
//console.log("test1");
let habitatArray = [];
if (habitatList.match(";")) {
habitatArray = habitatList.split(/\;\s*/);
} else {
habitatArray = [habitatList];
}
//console.log("test2 also here's an array",habitatArray);
let habitatTable = {};
habitatArray.forEach(category => {
//console.log("is this silently erroring or what");
let mainModifier = category.match(/^(\w*):/)[1];
//console.log("is the error here?");
let biomeSublistString = category.match(/:\s*(.+)$/)[1];
//console.log("maybe here? also biomeSublistString is equal to",biomeSublistString);
let biomeSublistArray = biomeSublistString.split(/\]*\,\s\[*/);
biomeSublistArray = biomeSublistArray.map(function(value) {
let subresult = value.match(/\[*([^\[\]]*)\]*(.+)$/);
let result = "";
if (subresult[2]) {
result = subresult[1]+subresult[2];
} else {
result = subresult[1];
}
return result;
});
//console.log("Biome sublist of "+species+":",biomeSublistArray);
habitatTable[mainModifier] = biomeSublistArray;
});
let newHabitatTable = {};
for (const [modifier, biome] of Object.entries(habitatTable)) {
//Detect parenthetical modifiers
let totalModifiers = "";
if (modifier != "Placeholder") {
totalModifiers = modifier;
}
biome.forEach(biomename => {
let splitup = biomename.match(/^\[*([^\[\]]+)\s\(([^\(\)]*)\)$/);
if (splitup != null) {
if (canontheticals.includes(splitup[2])) {
newHabitatTable[biomename] = totalModifiers;
} else if (totalModifiers == "") {
newHabitatTable[splitup[1]] = splitup[2];
} else {
newHabitatTable[splitup[1]] = totalModifiers+", "+splitup[2];
}
} else {
newHabitatTable[biomename.match(/^\[*([^\[\]]+)/)[1]] = totalModifiers;
}
});
}
//console.log("Biomes with their modifiers for "+species+":",newHabitatTable);
//do ecosystem page check
let invalidBiomes = [];
let validBiomes = [];
let addedTo = [];
let alreadyIn = [];
//let pagesOfBiomes = {};
for (const [biome,modifier] of Object.entries(newHabitatTable)) {
//subpageContents remember when I generated THAt a zillion lines ago?
//well that's because I wanted to get all api calls that did not need to be repeated out of the way before the loop
let editedBiomeName = "";
//To do: add code for getting weird names
let escapedbiome = biome.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let biomeregex = new RegExp("^[^\\w\\s]*"+escapedbiome+"[^\\w\\s]*$","i");
//console.log(escapedbiome,biomeregex);
for (const [subpage,content] of Object.entries(subpageContents)) {
//console.log(content);
if (content.some(e => biomeregex.test(e)) == false && invalidBiomes.includes(biome) == false && validBiomes.includes(biome) == false) {
//console.warn("invalid biome detected");
invalidBiomes.push(biome);
} else if (validBiomes.includes(biome) == true) {
invalidBiomes = invalidBiomes.filter(item => item !== biome);
} else if (content.some(e => biomeregex.test(e)) == true) {
invalidBiomes = invalidBiomes.filter(item => item !== biome);
validBiomes.push(biome);
//pagesOfBiomes[biome] = subpage; //so I don't have to do more code later LOL
//or I could just. do all that code I wanna do in here...
//find whole content of biome and check if species is already there
let modstring = "";
if (modifier != "") {
modstring = " ("+modifier+")";
}
let biomedex = content.findIndex(e => biomeregex.test(e));
let numberofequalsigns = content[biomedex].match(/^(=+)[^=]/)[1].length;
let chop = content.slice(biomedex + 1);
let endbiomedex = chop.findIndex(e => e.match(/=\[\[|noinclude/));
endbiomedex = endbiomedex + biomedex;
//^, Finds an equal or higher heading, or the end of the string. It does the former using...the number of equal signs. this feels stupid
let speciesdex = chop.indexOf("|"+species+modstring);
//console.log("Speciesdex:",speciesdex);
if (endbiomedex != -1 && biomedex != -1) {
if (speciesdex + biomedex < endbiomedex && speciesdex + biomedex > biomedex && speciesdex != -1) {
//console.log(species+" is already present in "+biome+" ✓");
//console.log("Biome index:",biomedex);
//console.log("Species index:",speciesdex + biomedex);
//console.log(content[speciesdex+biomedex+1]);
//console.log("End of biome index:",endbiomedex);
alreadyIn.push(biome);
} else {
let suitableSection = false; //so I can skip unsuitable sections until a suitable one is found
//let suitableSearchType = "init";
let groupskip = false;
let newbiome = false;
let microfactor = false;
for (var index = 0; index < content.length; index++) {
let line = content[index];
//Start from the index of the line the biome was on and go down the page until the appropriate location for the species is found
let spotfound = false;
if (index >= biomedex && index <= endbiomedex) {
if (suitableSection == false) {
//attempt to determine if a suitable section has been discovered
switch (true) {
case /NONE/.test(line):
newbiome = true;
//suitableSection = true;
//let regionName = subpage.match(/\/.*$/)[0];
//line = "{{#invoke:EcoData|main|biome="+biome+"\n\n|"+species+modstring+"\n\n}}{{topreturn|"+regionName+"}}";
//spotfound = true;
break;
case /groups/i.test(line):
groupskip = true;
break;
case /1 Micrometer/.test(line):
if (sizeclass == "1+ micrometers") {
suitableSection = true;
}
break;
case /10 Micrometer/.test(line):
if (sizeclass == "10+ micrometers") {
suitableSection = true;
}
break;
case /100 Micrometer/.test(line):
if (sizeclass == "100+ micrometers") {
suitableSection = true;
}
break;
case /1 Millimeter|1000 Micrometer/.test(line):
if (sizeclass == "1+ millimeters") {
suitableSection = true;
}
break;
case /1 Centimeter|10 Millimeter/.test(line):
if (sizeclass == "1+ centimeters") {
suitableSection = true;
}
break;
case /10 Centimeter/.test(line):
if (sizeclass == "10+ centimeters") {
suitableSection = true;
}
break;
case /20 Centimeter/.test(line):
if (sizeclass == ">20 centimeters") {
suitableSection = true;
}
break;
case /micro/i.test(line):
//Micro section
if (groupskip == false) {
microfactor = true;
}
if (micromacro == "micro" && groupskip == false) {
suitableSection = true;
} else if (micromacro == "micro" && groupskip == true) {
if (sizeclass == "<1 micrometer") {
suitableSection = true;
}
}
break;
case /macro/i.test(line):
//Macro section
if (micromacro == "macro" && groupskip == false) {
suitableSection = true;
}
break;
case /invoke\:EcoData/i.test(line):
if (microfactor == false && groupskip == false) {
suitableSection = true;
}
break;
default:
//no suitability found
}
if (newbiome == true) {
let regionName = subpage.match(/\/.*$/)[0];
suitableSection = true;
line = "{{#invoke:EcoData|main|biome="+biome+"\n\n|"+species+modstring+"\n\n}}{{topreturn|"+regionName+"}}";
spotfound = true;
break;
}
} else {
//If it's in a suitable section, find a suitable line
// /^\|/.test(line) will be true if it's a species
if (/^\|/.test(line) == true) {
//see if the species is in the original list, and if its index is higher than that of the current species
let foundspecies = line.match(/^\|([^\(\)])(\s\(.*\))?/)[1];
if (speciesList.includes(foundspecies)) {
if (speciesList.indexOf(species) < speciesList.indexOf(foundspecies)) {
//Suitable spot found! Add species and end list
content[index] = "|"+species+modstring+"\n\n"+line;
spotfound = true;
}
}
} else if (/\}\}/.test(line)) {
//list ended, so append the species to it
content[index] = "|"+species+modstring+"\n\n"+line;
spotfound = true;
}
}
if (spotfound == true) {
//modify page content and break out
let tempdata = "";
content.forEach((line2,index2) => {
tempdata = tempdata+"\n"+line2;
});
tempdata = tempdata.replace(/^\n*/,"");
subpageContents[subpage] = tempdata.split("\n");
addedTo.push(biome);
break;
}
} else if (index > endbiomedex) {
if (spotfound == false) {
console.error("Could not find a suitable location to insert "+species+" into "+biome+". Debug data: ",sizeclass,micromacro);
}
break;
}
}
//add the species to the suitable spot.
//addedTo.push(biome);
}
} else {
console.error("Something broke with the indexes while checking if "+species+" is in "+biome+".");
}
}
}
}
if (alreadyIn.length > 0) {
console.log(species+" was already present in: ",alreadyIn);
}
if (addedTo.length > 0) {
console.log(species+" was added to: ",addedTo);
}
if (invalidBiomes.length !== 0) {
console.warn("Some biomes not found for "+species+":",invalidBiomes);
} //else {
//console.log("No invalid biomes found for "+species+" ✓");
//}
}//this is the end bracket of the loop
console.log("Modified subpage contents:",subpageContents);
//save all pages
for (const [subpage,content] of Object.entries(subpageContents)) {
let contentMerged = "";
content.forEach(line => {
contentMerged = contentMerged + "\n" + line;
});
contentMerged = contentMerged.replace(/^\n*/, "");
api.postWithToken('csrf', {
headers: {
'Content-Type': 'multipart/form-data'
},
action: "edit",
title: subpage,
text: contentMerged,
summary: "Added species using JSaganBot",
format: 'json'
}).done(function(data){
console.log("Saved "+subpage+" successfully");
}).fail(function(error){
console.error(error);
});
}
}).catch(error => {
console.warn("There was an error: ",error);
});
});
}
});
}
};
//if (mw.config.get('wgCanonicalNamespace')+':'+mw.config.get('wgTitle') === 'Project:JSaganBot/Script' && mw.config.get('wgAction') == 'view')
//undecided if I'll stick to console scripts or make a page, I'm more comfortable with the console so I'll probably make it console-based and add a fancy gui later
console.log("JSaganBot version Attercop successfully initialized");
console.warn("JSaganBot is a work in progress. Be prepared to scramble to undo an edit that did something you didn't want.");