Skip to content

Commit 65fe204

Browse files
committed
feat: add spam penalty threshold for leaderboard ranking
- Add configurable spamPenaltyThreshold to demote spam-heavy contributors - Add POST /setSpamPenaltyThreshold admin endpoint (UI-driven per reviewer feedback) - Expose spamPenaltyThreshold in GET /config response - Pass threshold to API.getRanks() in /rank and /contributor endpoints - Apply penalty sort in frontend (fetch config before data) - Add spamPenaltyThreshold to config-example.json
1 parent 6195907 commit 65fe204

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

src/index.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import axios from 'axios'
44
import moment from 'moment'
55
import { io } from 'socket.io-client'
66

7+
let spamPenaltyThreshold = 0
8+
79
function refreshTable(newData) {
810
const table = document.querySelector('table')
911
const data = newData
@@ -24,6 +26,15 @@ function refreshTable(newData) {
2426
totalEm.innerText = 'Total: ' + totalNumbers
2527

2628
contributors = contributors.sort((a, b) => {
29+
if (spamPenaltyThreshold > 0) {
30+
const aPenalized = a.openPRsNumber >= spamPenaltyThreshold || a.issuesNumber >= spamPenaltyThreshold
31+
const bPenalized = b.openPRsNumber >= spamPenaltyThreshold || b.issuesNumber >= spamPenaltyThreshold
32+
const aTopTier = !aPenalized && a.mergedPRsNumber > 0
33+
const bTopTier = !bPenalized && b.mergedPRsNumber > 0
34+
if (aTopTier && !bTopTier) return -1
35+
if (!aTopTier && bTopTier) return 1
36+
}
37+
2738
var pref1, pref2, pref3 // preference is specified here
2839
const queryString = window.location.search
2940
const urlParams = new URLSearchParams(queryString)
@@ -145,18 +156,18 @@ function refreshTable(newData) {
145156
allContributionsInfoRef.innerText = allMergedPRs + ' Merged PRs, ' + allOpenPRs + ' Open PRs, and ' + allIssues + ' Issues.'
146157
}
147158

148-
axios.get('/api/data')
149-
.then(res => {
150-
refreshTable(res.data)
151-
})
152-
153159
axios.get('/api/config')
154160
.then(res => {
155161
const { organization, organizationGithubUrl, organizationHomepage } = res.data
162+
spamPenaltyThreshold = res.data.spamPenaltyThreshold || 0
156163
const footer = document.querySelector('.footer .text-muted')
157164
footer.innerHTML = `
158165
<a href="${organizationHomepage}" target="_blank" rel="noopener noreferrer">${organizationHomepage}</a> |
159166
<a href="${organizationGithubUrl}" target="_blank" rel="noopener noreferrer">Github(${organization})</a>`.trim()
167+
return axios.get('/api/data')
168+
})
169+
.then(res => {
170+
refreshTable(res.data)
160171
})
161172

162173
axios.get('/api/log')

src/server/app.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const server = http
9393
organization: organization,
9494
organizationHomepage: organizationHomepage,
9595
organizationGithubUrl: organizationGithubUrl,
96+
spamPenaltyThreshold: parseInt(jsonfile.readFileSync(configPath).spamPenaltyThreshold) || 0,
9697
})
9798
)
9899
break
@@ -238,6 +239,27 @@ const server = http
238239
}
239240
})
240241
break
242+
case '/setSpamPenaltyThreshold':
243+
if (req.method === 'GET') {
244+
res.end('Permission denied\n')
245+
return
246+
}
247+
248+
Util.post(req, (params) => {
249+
const { token, spamPenaltyThreshold } = params
250+
251+
if (token !== adminPassword) {
252+
res.end(JSON.stringify({ message: 'Authentication failed' }))
253+
} else {
254+
const Config = jsonfile.readFileSync(configPath)
255+
Config.spamPenaltyThreshold = parseInt(spamPenaltyThreshold) || 0
256+
jsonfile.writeFileSync(configPath, Config, { spaces: 2 })
257+
jsonfile.writeFileSync(configBackupPath, Config, { spaces: 2 })
258+
259+
res.end(JSON.stringify({ message: 'Success' }))
260+
}
261+
})
262+
break
241263
case '/remove':
242264
if (req.method === 'GET') {
243265
res.end('Permission denied\n')
@@ -350,10 +372,11 @@ const server = http
350372
jsonfile.readFile(dataPath, async (err, obj) => {
351373
if (err) console.log('[ERROR]' + err)
352374
const query = url.parse(req.url, true).query
375+
const Config = jsonfile.readFileSync(configPath)
353376

354377
// Gets list of contributors sorted by parameter if provided
355378
// else defaults to sorting by mergedprs
356-
const contributors = await API.getRanks(obj, query.parameter)
379+
const contributors = await API.getRanks(obj, query.parameter, Config.spamPenaltyThreshold)
357380

358381
// Responds with rank of username
359382
if (query.username) {
@@ -400,7 +423,8 @@ const server = http
400423
} else if (query.rank) {
401424
// Gets list of contributors sorted by parameter if provided
402425
// else defaults to sorting by mergedprs
403-
const contributors = await API.getRanks(obj, query.parameter)
426+
const Config = jsonfile.readFileSync(configPath)
427+
const contributors = await API.getRanks(obj, query.parameter, Config.spamPenaltyThreshold)
404428
res.end(JSON.stringify(obj[contributors[query.rank - 1]]))
405429
} else {
406430
res.end(JSON.stringify(obj))

src/server/config-example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"abdelazeem777\t"
135135
],
136136
"startDate": "1970-01-15",
137+
"spamPenaltyThreshold": 10,
137138
"includedRepositories": [
138139
"Rocket.Chat",
139140
"Opensource-Contribution-Leaderboard",

src/server/util/API.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ async function getStats(data) {
186186
}
187187
}
188188

189-
async function getRanks(data, parameter = 'mergedprs') {
189+
async function getRanks(data, parameter = 'mergedprs', spamPenaltyThreshold = 0) {
190190
var pref1, pref2, pref3 // preference is specified here
191191
switch (
192192
parameter //assigns according to parameter-sort (default 'mergedprs')
@@ -209,8 +209,19 @@ async function getRanks(data, parameter = 'mergedprs') {
209209
break
210210
}
211211

212+
const threshold = parseInt(spamPenaltyThreshold) || 0
213+
212214
const contributors = Object.keys(data)
213215
return contributors.sort((a, b) => {
216+
if (threshold > 0) {
217+
const aPenalized = data[a].openPRsNumber >= threshold || data[a].issuesNumber >= threshold
218+
const bPenalized = data[b].openPRsNumber >= threshold || data[b].issuesNumber >= threshold
219+
const aTopTier = !aPenalized && data[a].mergedPRsNumber > 0
220+
const bTopTier = !bPenalized && data[b].mergedPRsNumber > 0
221+
if (aTopTier && !bTopTier) return -1
222+
if (!aTopTier && bTopTier) return 1
223+
}
224+
214225
if (data[a][pref1] < data[b][pref1]) {
215226
return 1
216227
}

0 commit comments

Comments
 (0)