Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions modules/VotingSystem/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{
"name": "VotingSystem",
"author": "Unknown",
"description": "Run vote rewards for your game server community through Takaro. VotingSystem gives players a /vote command and gives admins a simple module foundation for rewarding voting activity.\n\nUse this module to:\n- Add a player-facing vote command.\n- Connect voting behavior to a community reward workflow.\n- Encourage players to support server visibility.\n- Keep vote-related commands inside Takaro's module system.\n\nGood fit for server owners who use voting sites, vote rewards, or community growth loops and want that workflow represented in Takaro.",
"version": "latest",
"supportedGames": [
"all"
],
"config": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"votingsite",
"votekey",
"novote",
"alreadyclaimed"
],
"additionalProperties": false,
"properties": {
"votingsite": {
"title": "votingsite",
"description": "Site where players vote. Example: \nhttps://7daystodie-servers.com for 7 Days to Die;\nhttps://rust-servers.net for RUST\n\n",
"default": "https://7daystodie-servers.com",
"type": "string"
},
"votekey": {
"title": "votekey",
"description": "Vote key provided by the voting site",
"type": "string"
},
"novote": {
"title": "novote",
"description": "Message to send to the player when voting site reports no vote was cast. ",
"default": "You haven't voted yet. Please vote at https://7daystodie-servers.com first.",
"type": "string"
},
"alreadyclaimed": {
"title": "alreadyclaimed",
"description": "Message to send to the player when the voting site reports the vote reward was already claimed.",
"default": "You have already claimed your last vote.",
"type": "string"
},
"privatemessage": {
"title": "privatemessage",
"description": "Optional message to send to the player when the voting site reports a vote with no reward claimed yet. \nExample: Thanks for voting.\nLeave it blank if you want nothing to be sent instead.",
"default": "",
"type": "string"
},
"publicmessage": {
"title": "publicmessage",
"description": "Optional message to broadcast to all players online when vote is successful. Use {name} in the message to refer to the player name.\nExample: [00FF00]{name}[-] voted at 7daystodie-servers.com and was rewarded Takaro Coins!\nLeave it blank if you want nothing to be sent instead.\n",
"default": "",
"type": "string"
},
"currency": {
"title": "currency",
"description": "How much currency to add to the player in Takaro, if any.\nMake sure to enable economy for this server.",
"default": 0,
"type": "number",
"minimum": 0
},
"randomitemnumber": {
"title": "randomitemnumber",
"description": "Defines how many random items will be rewarded to the player, picked from randomitemlist config field.\nThe quantities for each item picked is defined on the mentioned config field.",
"default": 0,
"type": "number",
"minimum": 0,
"multipleOf": 1
},
"randomitemlist": {
"title": "Items",
"description": "List of items that will be used to select a random reward for the vote, as many times as defined at randomitemnumber.",
"x-component": "item",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"title": "Item",
"properties": {
"item": {
"type": "string",
"title": "Item"
},
"amount": {
"type": "number",
"title": "Amount",
"minimum": 1,
"default": 1,
"multipleOf": 1
},
"quality": {
"type": "string",
"title": "Quality"
}
},
"required": [
"item"
],
"additionalProperties": false
},
"default": []
},
"fixedrewards": {
"title": "Items",
"description": "All the items in this list will be given as a reward for the vote.",
"x-component": "item",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"title": "Item",
"properties": {
"item": {
"type": "string",
"title": "Item"
},
"amount": {
"type": "number",
"title": "Amount",
"minimum": 1,
"default": 1,
"multipleOf": 1
},
"quality": {
"type": "string",
"title": "Quality"
}
},
"required": [
"item"
],
"additionalProperties": false
},
"default": []
}
}
},
"uiSchema": {
"randomitemlist": {
"items": {
"item": {
"ui:widget": "item"
}
}
},
"fixedrewards": {
"items": {
"item": {
"ui:widget": "item"
}
}
}
},
"commands": {
"vote": {
"trigger": "vote",
"description": "Claim rewards for voting for the server",
"helpText": "Claim rewards for voting for the server",
"function": "src/commands/vote/index.js",
"arguments": []
}
}
}
226 changes: 226 additions & 0 deletions modules/VotingSystem/src/commands/vote/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { takaro, data, axios } from '@takaro/helpers';

function buildVoteApiUrl(votingsite, params) {
const baseUrl = new URL(votingsite);
if (!['http:', 'https:'].includes(baseUrl.protocol)) {
throw new Error('Voting site URL must use http or https.');
}

const apiUrl = new URL('/api/', baseUrl);
for (const [key, value] of Object.entries(params)) {
apiUrl.searchParams.set(key, String(value));
}

return apiUrl.toString();
}

function sanitizeError(error) {
return {
message: error?.message,
status: error?.response?.status,
statusText: error?.response?.statusText,
responseData:
typeof error?.response?.data === 'string'
? error.response.data.slice(0, 200)
: error?.response?.data,
};
}

function getRewardAmount(itemConfig, fallbackAmount = 1) {
const amount = itemConfig?.amount ?? fallbackAmount;
if (!Number.isInteger(amount) || amount < 1) {
throw new Error('Vote reward item amounts must be whole numbers of at least 1.');
}

return amount;
}

async function resolveConfiguredItem(itemConfig, fallbackAmount = 1) {
if (typeof itemConfig === 'string') {
return {
gameItemName: itemConfig,
displayName: itemConfig,
amount: fallbackAmount,
quality: '0',
};
}

if (!itemConfig?.item) {
throw new Error('Vote reward item config is missing an item.');
}

const item = (await takaro.item.itemControllerFindOne(itemConfig.item)).data.data;
const amount = getRewardAmount(itemConfig, fallbackAmount);

return {
gameItemName: item.id,
displayName: item.name,
amount,
quality: itemConfig.quality ?? '',
};
}

async function giveResolvedItem(item) {
try {
await takaro.gameserver.gameServerControllerGiveItem(data.gameServerId, data.player.id, {
name: item.gameItemName,
amount: item.amount,
quality: item.quality,
});
} catch (error) {
// Some games do not support item quality. Retry without quality before failing the reward.
await takaro.gameserver.gameServerControllerGiveItem(data.gameServerId, data.player.id, {
name: item.gameItemName,
amount: item.amount,
quality: '',
});
}
}

function selectRandomRewards(randomitemnumber, randomitemlist = []) {
const availableRandomRewards = [...randomitemlist];
const requestedCount = Number.isInteger(randomitemnumber) ? randomitemnumber : 0;
const actualRandomItemNumber = Math.min(Math.max(requestedCount, 0), availableRandomRewards.length);
const selectedRewards = [];

for (let i = 0; i < actualRandomItemNumber; i++) {
const randomIndex = Math.floor(Math.random() * availableRandomRewards.length);
selectedRewards.push(availableRandomRewards.splice(randomIndex, 1)[0]);
}

return selectedRewards;
}

async function main() {
const {
votingsite,
votekey,
novote,
alreadyclaimed,
privatemessage,
publicmessage,
currency,
randomitemnumber,
randomitemlist = [],
fixedrewards = [],
} = data.module.userConfig;
const { steamId } = data.player;

const voteIdentity = steamId ? { steamid: steamId } : { username: data.player.name };

let votingSiteResponse;
try {
votingSiteResponse = String(
(
await axios.get(
buildVoteApiUrl(votingsite, {
object: 'votes',
element: 'claim',
key: votekey,
...voteIdentity,
}),
)
).data,
);
} catch (error) {
console.log('Failed to check vote status:', sanitizeError(error));
await data.player.pm('Could not check your vote right now. Please try again later.');
return;
}

if (votingSiteResponse === 'Error: server key not found') {
await data.player.pm('The server vote key is invalid, please notify the admins.');
return;
}
if (votingSiteResponse === '0') {
await data.player.pm(novote);
return;
}
if (votingSiteResponse === '2') {
await data.player.pm(alreadyclaimed);
return;
}
if (votingSiteResponse !== '1') {
console.log('Unexpected vote status response:', votingSiteResponse);
await data.player.pm('The voting site returned an unexpected response. Please notify the admins.');
return;
}

let itemRewards;
try {
const randomRewards = selectRandomRewards(randomitemnumber, randomitemlist);
itemRewards = await Promise.all([...randomRewards, ...fixedrewards].map((reward) => resolveConfiguredItem(reward)));
} catch (error) {
console.log('Vote reward configuration is invalid:', sanitizeError(error));
await data.player.pm('Vote rewards are not configured correctly. Please notify the admins.');
return;
}

let claimResponse;
try {
claimResponse = String(
(
await axios.post(
buildVoteApiUrl(votingsite, {
action: 'post',
object: 'votes',
element: 'claim',
key: votekey,
...voteIdentity,
}),
)
).data,
);
} catch (error) {
console.log('Failed to claim vote reward:', sanitizeError(error));
await data.player.pm('Something went wrong while claiming your vote reward. Please try again later.');
return;
}

if (claimResponse !== '1') {
await data.player.pm('Something went wrong while claiming your vote reward.');
console.log('Unexpected vote claim response:', claimResponse);
return;
}

if (privatemessage) {
await data.player.pm(privatemessage);
}
if (publicmessage) {
await takaro.gameserver.gameServerControllerSendMessage(data.gameServerId, {
message: publicmessage.replace('{name}', data.player.name),
});
}

const rewardErrors = [];
const totalItemRewards = itemRewards.length;
for (let i = 0; i < itemRewards.length; i++) {
const item = itemRewards[i];
try {
await giveResolvedItem(item);
await data.player.pm(`You received ${item.amount}x ${item.displayName}! (item ${i + 1}/${totalItemRewards})`);
} catch (error) {
rewardErrors.push(`item ${item.displayName}`);
console.log('Failed to give vote item reward:', sanitizeError(error));
}
}

if (currency > 0) {
try {
const currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', data.gameServerId)).data.data;
await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(data.gameServerId, data.player.id, {
currency,
});
await data.player.pm(`You received ${currency} ${currencyName.value}.`);
} catch (error) {
rewardErrors.push('currency');
console.log('Failed to give vote currency reward:', sanitizeError(error));
}
}

if (rewardErrors.length > 0) {
await data.player.pm('Your vote was claimed, but some rewards failed. Please notify the admins.');
}
}

await main();
Loading