"use strict"; /// registerPlugin({ name: "Vote Reward", engine: ">= 1.0.0", version: "1.0.0", description: "Group Vote Rewards for TeamSpeakServers.org", author: "Multivitamin { const { key, sid, removeTime, rewards } = config; let initialized = false; const rewardSorted = rewards.sort((g1, g2) => g1.amount - g2.amount).reverse(); const availableGroups = rewards.map(g => g.group); const removeAfter = removeTime > 0 ? removeTime * 24 * 60 * 60 : -1; const engine = require("engine"); const event = require("event"); const store = require("store"); const http = require("http"); const backend = require("backend"); const helpers = require("helpers"); const CHECK_INTERVAL = 60 * 1000; class Vote { /** initializes store */ init() { if (!Array.isArray(this.getVotes())) this.setVotes([]); } /** current store namespace */ ns(name) { return `${this.namespace}${name}`; } /** retrieves the namespace for vote items */ get nsVotes() { return this.ns("votes"); } /** retrieves all votes from store */ getVotes() { return store.getInstance(this.nsVotes); } /** saves all vote items back to store */ setVotes(items) { store.setInstance(this.nsVotes, items); } /** retrieves all votes */ saveItem(item) { this.setVotes(this.getVotes().map(i => i.hash === item.hash ? item : i)); return this; } addItem(item) { this.setVotes([...this.getVotes(), item]); return this; } /** requests to add an item to the store */ requestAdd(item) { const hash = this.getHash(item); if (this.findHash(hash)) return false; if (this.isOld(item)) return false; const newItem = this.createVoteItem(item); this.addItem(newItem); this.tryMakeClaim(newItem); return true; } /** checks if the item is too old to get added or still be hold in store */ isOld(item) { if (removeAfter === -1) return false; return item.timestamp < Math.floor(Date.now() / 1000) - removeAfter; } /** retrieves the hash value of an item */ getHash(item) { return helpers.MD5Sum(`${item.nickname}${item.timestamp}`); } /** finds an item with a specific hash */ findHash(hash) { return this.getVotes().find((item) => item.hash === hash); } /** handles a full client check */ checkClient(client) { this.getUnclaimedByNickname(client.nick()).forEach(item => this.tryMakeClaim(item, client)); this.checkGroups(client); } /** tries to claim a possible not claimed item */ tryMakeClaim(item, client) { if (!this.isUnclaimed(item)) return false; client = client ? client : this.getClientByItem(item); if (!client) return false; engine.log(`Client ${client.nick()} (${client.uid()}) claims a vote (${item.hash})`); this.flagItemClaimed(item, client.uid()); this.saveItem(item); this.checkGroups(client); return true; } /** checks wether an item is unclaimed or not */ isUnclaimed(item) { return item.claimedBy === null; } /** tries to retrieve the client for which the vote is for */ getClientByItem(item) { return backend.getClientByName(item.nickname); } /** validates the groups a client has */ checkGroups(client) { const group = this.getGroupFromVoteCount(this.getVotesByClient(client).length); if (group === -1) return; return this.whiteListGroup(client, [group], availableGroups); } /** * adds a set of groups to a client and removes groups he should not be in * @param client the client to add/remove groups from * @param group the groups a client can have * @param whitelisted whitelisted groups */ whiteListGroup(client, groups, whitelisted) { let assign = groups.map(g => String(g)); const remove = whitelisted.map(w => String(w)).filter(w => !assign.includes(w)); client.getServerGroups().forEach(group => { if (remove.includes(group.id())) { client.removeFromServerGroup(group.id()); } else if (assign.includes(group.id())) { assign.splice(assign.indexOf(group.id()), 1); } }); assign.forEach(g => client.addToServerGroup(g)); } /** * retrieves the servergroup the amount of counts should get * @param votes the votecount to check */ getGroupFromVoteCount(votes) { let g = rewardSorted.find(g => g.amount <= votes); if (!g) { if (rewardSorted.length === 0 || rewardSorted[0].amount > votes) { g = { amount: -1, group: -1 }; } else { g = rewardSorted[rewardSorted.length - 1]; } } return g.group; } /** * retrieves the vote items a client has been assigned * @param client the client to retrieve */ getVotesByClient(client) { return this.getVotes().filter(item => item.claimedBy === client.uid()); } /** * gets all unclaimed votes a client nickname can be assigned to * @param nick the nickname to check */ getUnclaimedByNickname(nick) { return this.getVotes() .filter(item => this.isUnclaimed(item)) .filter(item => item.nickname === nick); } /** * removes all items which are older than the days given in config */ cleanOldItems() { const votes = this.getVotes(); const cleaned = votes.filter(item => !this.isOld(item)); if (votes.length === cleaned.length) return; this.setVotes(cleaned); } /** * interval checks */ cron() { this.cleanOldItems(); this.check(); } /** * sets an items status to claimed * @param item the item which should get claimed * @param uid the uid which claimed the item */ flagItemClaimed(item, uid) { item.claimedBy = uid; item.claimedAt = Date.now(); return this; } /** * creates a fully valid VoteItem * @param item the item which should be upgraded */ createVoteItem(item) { return { ...item, added: Date.now(), hash: this.getHash(item), claimedBy: null, claimedAt: 0 }; } } class TeamSpeakServers extends Vote { constructor({ key, sid, createCommand }) { super(); this.namespace = "teamspeakServersDotOrg_"; this.apikey = key; this.sid = sid; this.registerCommand(createCommand); this.init(); } registerCommand(createCommand) { createCommand("vote") .help("retrieves the vote link from teamspeak-servers.org") .manual("retrieves the vote link for teamspeak-servers.org") .manual(`vote daily to get rewarded with servergroups!`) .exec((client, _, reply) => { reply(`[b][url=https://teamspeak-servers.org/server/${this.sid}/vote/?username=${encodeURI(client.nick())}]VOTE HERE[/url]`); reply(`It can take a few minutes until your vote gets counted!`); if (removeAfter === -1) { reply(`You have have voted ${this.getVotesByClient(client).length} times!`); } else { reply(`You have have voted ${this.getVotesByClient(client).length} times in the last ${removeTime} days!`); } }); } async check() { const votes = await this.fetchVotes(); votes.forEach(vote => this.requestAdd({ nickname: vote.nickname, timestamp: vote.timestamp })); } fetchVotes() { return new Promise((fulfill, reject) => { http.simpleRequest({ method: "GET", url: `https://teamspeak-servers.org/api/?object=servers&element=votes&key=${this.apikey}&format=json` }, (err, res) => { if (err) return reject(new Error(`Failed to retrieve data from teamspeak-servers.org api! (Error ${err})`)); if (res.statusCode !== 200) return reject(new Error(`Failed to retrieve data from teamspeak-servers.org api! (Code ${res.statusCode})`)); try { fulfill(JSON.parse(res.data.toString()).votes); } catch (e) { engine.log(`got response from teamspeak-servers.org: ${res.data.toString()}`); return reject(e); } }); }); } } const votings = []; function doGlobalCheck() { votings.forEach(vote => vote.cron()); backend.getClients() .filter(c => !c.isSelf()) .forEach(c => votings.forEach(v => v.checkClient(c))); } event.on("connect", () => { if (initialized) return; initialized = true; doGlobalCheck(); }); event.on("disconnect", () => { initialized = false; }); event.on("clientMove", ({ fromChannel, client }) => { if (fromChannel || client.isSelf()) return; votings.forEach(v => v.checkClient(client)); }); event.on("clientNick", client => votings.forEach(v => v.checkClient(client))); event.on("serverGroupAdded", ev => votings.forEach(v => v.checkClient(ev.client))); event.on("serverGroupRemoved", ev => votings.forEach(v => v.checkClient(ev.client))); setInterval(() => { if (!backend.isConnected()) return; votings.forEach(vote => vote.cron()); }, CHECK_INTERVAL); event.on("load", () => { const command = require("command"); const { createCommand } = command; votings.push(new TeamSpeakServers({ key, sid, createCommand })); if (backend.isConnected()) { initialized = true; doGlobalCheck(); } }); });