-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Show survivor count and vote events on infoscreen (#29)
Creates an infoscreen entry each approved vote, and another one that contains results when the vote ends. Adds an infoscreen entry that shows the current survivor count. --------- Co-authored-by: Nico Hagelberg <[email protected]>
- Loading branch information
1 parent
fef58e2
commit 83e07f3
Showing
10 changed files
with
228 additions
and
20 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
db/migrations/20240509151944_add-active-until-infoboard.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Knex } from "knex"; | ||
|
||
|
||
export async function up(knex: Knex): Promise<void> { | ||
return knex.schema.alterTable('infoboard_entry', (table) => { | ||
table.timestamp('active_until').nullable(); | ||
}); | ||
} | ||
|
||
|
||
export async function down(knex: Knex): Promise<void> { | ||
return knex.schema.alterTable('infoboard_entry', (table) => { | ||
table.dropColumn('active_until'); | ||
}); | ||
} |
15 changes: 15 additions & 0 deletions
15
db/migrations/20240615063627_add-infoboard-entry-details.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Knex } from 'knex'; | ||
|
||
export async function up(knex: Knex): Promise<void> { | ||
return knex.schema.alterTable('infoboard_entry', table => { | ||
table.string('identifier').nullable().unique(); | ||
table.jsonb('metadata').nullable(); | ||
}); | ||
} | ||
|
||
export async function down(knex: Knex): Promise<void> { | ||
return knex.schema.alterTable('infoboard_entry', table => { | ||
table.dropColumn('metadata'); | ||
table.dropColumn('identifier'); | ||
}); | ||
} |
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
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
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { interval } from '../helpers'; | ||
import { logger } from '../../logger'; | ||
import { InfoEntry } from '../../models/infoentry'; | ||
import { getTotalSoulsAlive } from '../../models/ship'; | ||
|
||
const POLL_FREQUENCY_MS = 10000; | ||
|
||
const closeInfoEntryTimers = new Map(); | ||
|
||
async function closeInfoEntry(infoEntry) { | ||
logger.info(`Setting InfoEntry '${infoEntry.get('title')}' as inactive`); | ||
await infoEntry.save({ enabled: false }, { method: 'update', patch: true }); | ||
} | ||
|
||
async function processScheduledInfoEntries() { | ||
const activeInfoEntries = await new InfoEntry() | ||
.where('enabled', true) | ||
.where('active_until', 'is not', null) | ||
.fetchAll(); | ||
closeInfoEntryTimers.forEach(clearTimeout); | ||
closeInfoEntryTimers.clear(); | ||
for (const infoEntry of activeInfoEntries.models) { | ||
const closesIn = new Date(infoEntry.get('active_until')) - Date.now(); | ||
|
||
// Close right away if already expired | ||
if (closesIn < 1) return await closeInfoEntry(infoEntry); | ||
|
||
// If there's more than 2x poll frequency remaining, don't bother with the interval | ||
if (closesIn > POLL_FREQUENCY_MS * 2) return; | ||
|
||
// Set an interval that closes the infoentry right when it's supposed to | ||
closeInfoEntryTimers.set( | ||
infoEntry.get('id'), | ||
setTimeout(() => closeInfoEntry(infoEntry), closesIn) | ||
); | ||
} | ||
} | ||
|
||
async function updateSurvivorsCount() { | ||
const [entry, totalSoulsAlive] = await Promise.all([ | ||
InfoEntry.forge({ identifier: 'survivors-count' }).fetch(), | ||
getTotalSoulsAlive(), | ||
]); | ||
if (!entry) { | ||
logger.warn("Survivors count info entry not found, can't update survivors count"); | ||
return; | ||
} | ||
const body = entry.get('body'); | ||
const replacement = `<span class="survivors">${totalSoulsAlive}</span>`; | ||
const pattern = /<span class="survivors">.*?<\/span>/; | ||
const updatedBody = body.replace(pattern, replacement); | ||
await entry.save({ body: updatedBody }, { method: 'update', patch: true }); | ||
} | ||
|
||
// Process info entries that are scheduled to close | ||
interval(processScheduledInfoEntries, POLL_FREQUENCY_MS); | ||
|
||
// Update survivors count periodically | ||
interval(updateSurvivorsCount, POLL_FREQUENCY_MS); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,74 @@ | ||
import { interval } from '../helpers'; | ||
import { logger } from '../../logger'; | ||
import { Vote } from '../../models/vote'; | ||
import { Vote, VoteEntry, VoteOption } from '../../models/vote'; | ||
import { InfoEntry } from '../../models/infoentry'; | ||
import moment from 'moment'; | ||
|
||
const POLL_FREQUENCY_MS = 10000; | ||
|
||
const closeVoteTimers = new Map(); | ||
|
||
function closeVote(vote) { | ||
async function closeVote(vote) { | ||
logger.info('Closing vote', vote.get('id')); | ||
return vote.save({ is_active: false }, { method: 'update', patch: true }); | ||
await vote.save({ is_active: false }, { method: 'update', patch: true }); | ||
await createVoteResultsInfoEntry(vote); | ||
} | ||
|
||
async function updateVotesScheduledToClose() { | ||
async function createVoteResultsInfoEntry(vote) { | ||
const infoEntry = new InfoEntry(); | ||
const activeMinutes = 60; | ||
const activeUntil = moment().add(activeMinutes, 'minutes').toDate(); | ||
const voteEntries = await new VoteEntry().where('vote_id', vote.get('id')).fetchAll(); | ||
const voteOptions = await new VoteOption().where('vote_id', vote.get('id')).fetchAll(); | ||
|
||
const title = 'Vote results: ' + vote.get('title'); | ||
let body = 'No votes were cast.'; | ||
if (voteEntries.length > 0) { | ||
body = 'Votes in total: ' + voteEntries.length + '</br>Winning vote option(s):'; | ||
const resultArray = []; | ||
let winnerVotes = 0; | ||
voteOptions.forEach(option => { | ||
const votes = voteEntries.filter(single => single.get('vote_option_id') === option.get('id')).length; | ||
if (votes > winnerVotes) { | ||
winnerVotes = votes; | ||
} | ||
resultArray.push({ result: votes, option: option.get('text') }); | ||
}); | ||
|
||
const winningVote = resultArray.find(({ result }) => result === winnerVotes); | ||
body += '</br>' + winningVote.option + ' : ' + winningVote.result; | ||
} | ||
|
||
const postData = { | ||
priority: 1, | ||
enabled: true, | ||
title: title, | ||
body: body, | ||
active_until: activeUntil, | ||
}; | ||
await infoEntry.save(postData, { method: 'insert' }); | ||
} | ||
|
||
async function processVotesScheduledToClose() { | ||
const activeVotes = await new Vote().where('is_active', true).fetchAll(); | ||
closeVoteTimers.forEach(timeout => clearTimeout(timeout)); | ||
closeVoteTimers.forEach(clearTimeout); | ||
closeVoteTimers.clear(); | ||
activeVotes.forEach(vote => { | ||
for (const vote of activeVotes.models) { | ||
const closesIn = new Date(vote.get('active_until')) - Date.now(); | ||
|
||
// Close right away if already expired | ||
if (closesIn < 1) return closeVote(vote); | ||
if (closesIn < 1) return await closeVote(vote); | ||
|
||
// If there's more than 2x poll frequency remaining, don't bother with the interval | ||
if (closesIn > POLL_FREQUENCY_MS * 2) return; | ||
|
||
// Set an interval that closes the vote right when it's supposed to | ||
closeVoteTimers.set(vote.get('id'), setTimeout(() => closeVote(vote), closesIn)); | ||
}); | ||
closeVoteTimers.set( | ||
vote.get('id'), | ||
setTimeout(() => closeVote(vote), closesIn) | ||
); | ||
} | ||
} | ||
|
||
// Update votes that are scheduled to close | ||
interval(updateVotesScheduledToClose, 10000); | ||
interval(processVotesScheduledToClose, 10000); |
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