Introduction
The wikidot karma system is of a mechanism unknown to users of wikidot. Rumours are heard that karma4 (very high, abbreviated "k4" below) and karma5 (guru, abbreviated "k5" below) requires 2000 revisions and 8000 revisions respectively.
In order to find out how such mechanism works, a series of data collection is carried out.
Concept
There is a speculation about wikidot karma system as follows:
The portion of k5 members out of total number of users is constant. Wikidot promotes the top k4 members to k5 when new users join wikidot and the number of k5 members had been raised according to the portion, and that new k5 slots are available; or when a k4 member has surpassed the relative threshold corresponding to said portion by their activity calculations.
Wikidot source code
The wikidot source code for karma calculation is available here with relevant rules here, which can be read.
First-hand data
A karma grinding script is developed and executed with karma level being monitored.
Second-hand data
The revision count and forum post count for accounts known to have k5 are extracted.
Method
Karma grinding script
A karma grinding script is developed in NodeJS, and executed on
Karma PoC.
The code is not disclosed to prevent abused usage, but the concepts are as follows:
- The script logs into the said account.
- The script sends an edit and save edit request every 3 seconds to a particular page that can be discarded easily.
- The script sends requests to post a forum post every 60 seconds to per page discussion of the said page.
- The script retrieves the karma level status of the said account every 60 seconds, and stops steps 2 & 3 from executing after k5 is reached.
Counting guru revisions and forum posts
A revision and forum post counting script is developed in NodeJS.
The code is as follows:
const got = require('got');
const cheerio = require('cheerio');
class WD {
https://github.com/resure/wikidot-ajax/blob/master/index.js
constructor(baseURL) {
this.baseURL = `${baseURL}/ajax-module-connector.php`;
}
async req(url, params) {
const wikidotToken7 = Math.random().toString(36).substring(4);
return await got.post(url, {
headers: {Cookie: `wikidot_token7=${wikidotToken7}`},
form: Object.assign({wikidot_token7: wikidotToken7, callbackIndex: 0}, params)
}).json();
};
async module(moduleName, params) {
return await this.req(this.baseURL, Object.assign({moduleName: moduleName},params))
}
async getUser(username) {
return await this.module("users/UserSearchModule", {
query: username
})
}
async getrevs(info, id, perpage, page) {
info = await this.module("userinfo/UserChangesListModule", {
userId: `${id}`,
perpage: `${perpage}`,
page: page
})
return {body: info.body, ex: info.body.trim()!==`Sorry, no revisions matching your criteria.`}
}
async getposts(info, id, perpage, page) {
info = await this.module("userinfo/UserRecentPostsListModule", {
userId: `${id}`,
limit: `${perpage}`,
page: page
})
return cheerio.load(info.body)
}
async linsearch(username, userId) {
if (!(userId||username)) return null;
if (!!userId) {var id = userId};
if (!userId && !!username) {
// The following retrieval of user id by username is avoided whenever user id is provided, as the search process is very slow.
var user = username.toLowerCase()
var res = await this.getUser(user);
var names = Object.values(res.userNames).map(x=>x.trim().toLowerCase());
var id = Object.keys(res.userNames)[names.indexOf(user)];
}
// If the user id is already known by other means the id can be directly inputted in the following line.
//var id = 1404533//zyn////3427263//7happy7////4476849//sekai_s////2941964//m element
var ret = {revisions: null, posts: null}
// Retrieving number of total revisions
var info1
var num1 = 5000, count1 = 0
while (num1==5000) {
info1 = await this.getrevs(info1, id, 5000, ++count1)
var $1 = cheerio.load(info1.body)
num1 = $1("tr").length
//console.log(`Got revisions ${$1("span.pager-no").first().text().trim()}`)
}
ret.revisions = (count1-1)*5000+num1;
// Retrieving number of total forum posts
var info2
var num2 = 3000, count2 = 0
while (num2==3000) {
var $2 = await this.getposts(info2, id, 3000, ++count2)
num2 = $2("div.post").length
//console.log(`Got forum posts ${$2("span.pager-no").first().text().trim()}`)
}
ret.posts = (count2-1)*3000+num2;
return ret;
}
async expsearch(username, userId) {
if (!(userId||username)) return null;
if (!!userId) {var id = userId};
if (!userId && !!username) {
// The following retrieval of user id by username is avoided whenever user id is provided, as the search process is very slow.
var user = username.toLowerCase()
var res = await this.getUser(user);
var names = Object.values(res.userNames).map(x=>x.trim().toLowerCase());
var id = Object.keys(res.userNames)[names.indexOf(user)];
}
// If the user id is already known by other means the id can be directly inputted in the following line.
//var id = 1404533//zyn////3427263//7happy7////4476849//sekai_s////2941964//m element
var ret = {revisions: null, posts: null}
// Retrieving number of total revisions
var info1={body:undefined, ex:true}, $1, page, hiin1=8, index1=8, count1=256, num1=20;
while (num1==20) {
info1 = await this.getrevs(info1, id, 20, count1);
$1 = cheerio.load(info1.body);
num1 = $1("tr").length;
page = $1("span.pager-no").first().text().trim()
if (page.split(" ").length!=4) {
if (index1<0) {index1=0};
if (info1.ex) {
if (index1>=hiin1) { count1*=2; hiin1++; index1++; }
else { count1+=2**index1; index1--; }
} else {
count1-=2**index1; index1--; num1=20;
}
} else {
info1 = await this.getrevs(info1, id, 20, page.split(" ").pop());
$1 = cheerio.load(info1.body);
num1 = $1("tr").length;
}
}
ret.revisions=(count1-1)*20+num1
// Retrieving number of total forum posts
var info2, $2, page, hiin2=7, index2=7, count2=128, num2=20;
while (num2==20||num2==0) {
$2 = await this.getposts(info2, id, 20, count2);
num2 = $2("div.post").length
page = $2("span.pager-no").first().text().trim()
if (index2<0) {index2=0};
if (num2) {
if (index2>=hiin2) { count2*=2; hiin2++; index2++; }
else { count2+=2**index2; index2--; }
} else {
count2-=2**index2; index2--;
}
}
ret.posts=(count2-2)*20+num2;
return ret;
}
}
var a = new WD("http:
var id = 1404533
a.linsearch(null, id).then(res=>console.log(res))
The code is also usable as a service at https://zh.xjo.ch/wd/userinfo , with query string parameters userId or user_id for wikidot user id, or username for wikidot username if user id is not provided.
Try using user id and avoid using username if revision amount is over 20000 or forum posts amount is over 10000 because converting username to user id takes an additional 10 seconds or so.
If revisions or forum posts are too many then the tool may error out with gateway timeout on server end.
Many thanks to
Unihedron for advice on using exponential search algorithm.
Results
Wikidot source code
As the wikidot source code is over 10 years old and may not reflect the current process of karma calculation accurately, it serves for reference purposes only.
By reading the wikidot source code, it can be observed that there are certain minimum activity scores that has to be reached in order to qualify for different karma levels, however the division of user karma levels are done according to proportions of the overall wikidot userbase. This confirms the speculation put forth in the concept section, at least for wikidot 10 years ago.
Karma grinding
Karma PoC reached k4 at around 2000 revisions and 0 posts.
Said account reached k5 at 11631 revisions and 101 posts.
A possible error: wikidot may be slow to update karma level, such that the script stops well after the threshold had been surpassed.
Statistics of current gurus
Sorted in alphabetical order
All data retrieved are of 03:30 10th May, 2020, UTC+8
Retrieved data may be inaccurate as deleted revisions and forum posts are not counted
User |
No. of revisions |
No. of forum posts |
7happy7 |
29917 |
653 |
djkaktus |
6871 |
2463 |
feitag |
9346 |
3244 |
Helmut_pdorf |
19940 |
8590 |
Ihp does not match any existing user name |
8472 |
2716 |
Karma PoC |
11631 |
101 |
Lt Flops |
10702 |
700 |
M Element |
15108 |
2699 |
NatVoltaic |
11197 |
1404 |
notgull |
5629 |
1752 |
pokm |
5685 |
2285 |
Rounderhouse |
11272 |
713 |
Sekai_s |
5489 |
629 |
Woedenaz |
8355 |
353 |
Zyn |
8410 |
48175 |
Conclusion
First, the 8000 revisions rumour is considered false, as it does not take into account the other factors that affect karma levels. Examples include
pokm and
Sekai_s at under 6000 revisions. As for first-hand example, k5 is only reached at about 11000 revisions and about 100 posts.
Second, regarding how exactly the karma system works, it can be said that the "proportional karma" speculation was true for the open-sourced wikidot source code from over 10 years ago, however there are no conclusions that can be drawn as of current, as there are multiple possible errors.