· 6 years ago · Mar 03, 2020, 03:16 PM
1"use strict";
2///<reference path="node_modules/sinusbot/typings/global.d.ts" />
3registerPlugin({
4 name: "Vote Reward",
5 engine: ">= 1.0.0",
6 version: "1.0.0",
7 description: "Group Vote Rewards for TeamSpeakServers.org",
8 author: "Multivitamin <david.kartnaller@gmail.com",
9 requiredModules: ["http"],
10 vars: [{
11 name: "key",
12 title: "TeamSpeak-Servers API Key",
13 type: "string",
14 default: ""
15 }, {
16 name: "sid",
17 title: "TeamSpeak-Servers Server ID",
18 type: "string",
19 default: ""
20 }, {
21 name: "removeTime",
22 title: "remove vote when older than x days (default: 30, -1 to disable removal)",
23 type: "number",
24 default: 30
25 }, {
26 name: "rewards",
27 title: "Group per vote",
28 type: "array",
29 vars: [{
30 name: "amount",
31 title: "Amount of Votes in this month",
32 type: "number",
33 default: 0
34 }, {
35 name: "group",
36 title: "Group Reward",
37 type: "number",
38 default: 0
39 }]
40 }]
41}, (_, config) => {
42 const { key, sid, removeTime, rewards } = config;
43 let initialized = false;
44 const rewardSorted = rewards.sort((g1, g2) => g1.amount - g2.amount).reverse();
45 const availableGroups = rewards.map(g => g.group);
46 const removeAfter = removeTime > 0 ? removeTime * 24 * 60 * 60 : -1;
47 const engine = require("engine");
48 const event = require("event");
49 const store = require("store");
50 const http = require("http");
51 const backend = require("backend");
52 const helpers = require("helpers");
53 const CHECK_INTERVAL = 60 * 1000;
54 class Vote {
55 /** initializes store */
56 init() {
57 if (!Array.isArray(this.getVotes()))
58 this.setVotes([]);
59 }
60 /** current store namespace */
61 ns(name) {
62 return `${this.namespace}${name}`;
63 }
64 /** retrieves the namespace for vote items */
65 get nsVotes() {
66 return this.ns("votes");
67 }
68 /** retrieves all votes from store */
69 getVotes() {
70 return store.getInstance(this.nsVotes);
71 }
72 /** saves all vote items back to store */
73 setVotes(items) {
74 store.setInstance(this.nsVotes, items);
75 }
76 /** retrieves all votes */
77 saveItem(item) {
78 this.setVotes(this.getVotes().map(i => i.hash === item.hash ? item : i));
79 return this;
80 }
81 addItem(item) {
82 this.setVotes([...this.getVotes(), item]);
83 return this;
84 }
85 /** requests to add an item to the store */
86 requestAdd(item) {
87 const hash = this.getHash(item);
88 if (this.findHash(hash))
89 return false;
90 if (this.isOld(item))
91 return false;
92 const newItem = this.createVoteItem(item);
93 this.addItem(newItem);
94 this.tryMakeClaim(newItem);
95 return true;
96 }
97 /** checks if the item is too old to get added or still be hold in store */
98 isOld(item) {
99 if (removeAfter === -1)
100 return false;
101 return item.timestamp < Math.floor(Date.now() / 1000) - removeAfter;
102 }
103 /** retrieves the hash value of an item */
104 getHash(item) {
105 return helpers.MD5Sum(`${item.nickname}${item.timestamp}`);
106 }
107 /** finds an item with a specific hash */
108 findHash(hash) {
109 return this.getVotes().find((item) => item.hash === hash);
110 }
111 /** handles a full client check */
112 checkClient(client) {
113 this.getUnclaimedByNickname(client.nick()).forEach(item => this.tryMakeClaim(item, client));
114 this.checkGroups(client);
115 }
116 /** tries to claim a possible not claimed item */
117 tryMakeClaim(item, client) {
118 if (!this.isUnclaimed(item))
119 return false;
120 client = client ? client : this.getClientByItem(item);
121 if (!client)
122 return false;
123 engine.log(`Client ${client.nick()} (${client.uid()}) claims a vote (${item.hash})`);
124 this.flagItemClaimed(item, client.uid());
125 this.saveItem(item);
126 this.checkGroups(client);
127 return true;
128 }
129 /** checks wether an item is unclaimed or not */
130 isUnclaimed(item) {
131 return item.claimedBy === null;
132 }
133 /** tries to retrieve the client for which the vote is for */
134 getClientByItem(item) {
135 return backend.getClientByName(item.nickname);
136 }
137 /** validates the groups a client has */
138 checkGroups(client) {
139 const group = this.getGroupFromVoteCount(this.getVotesByClient(client).length);
140 if (group === -1)
141 return;
142 return this.whiteListGroup(client, [group], availableGroups);
143 }
144 /**
145 * adds a set of groups to a client and removes groups he should not be in
146 * @param client the client to add/remove groups from
147 * @param group the groups a client can have
148 * @param whitelisted whitelisted groups
149 */
150 whiteListGroup(client, groups, whitelisted) {
151 let assign = groups.map(g => String(g));
152 const remove = whitelisted.map(w => String(w)).filter(w => !assign.includes(w));
153 client.getServerGroups().forEach(group => {
154 if (remove.includes(group.id())) {
155 client.removeFromServerGroup(group.id());
156 }
157 else if (assign.includes(group.id())) {
158 assign.splice(assign.indexOf(group.id()), 1);
159 }
160 });
161 assign.forEach(g => client.addToServerGroup(g));
162 }
163 /**
164 * retrieves the servergroup the amount of counts should get
165 * @param votes the votecount to check
166 */
167 getGroupFromVoteCount(votes) {
168 let g = rewardSorted.find(g => g.amount <= votes);
169 if (!g) {
170 if (rewardSorted.length === 0 || rewardSorted[0].amount > votes) {
171 g = { amount: -1, group: -1 };
172 }
173 else {
174 g = rewardSorted[rewardSorted.length - 1];
175 }
176 }
177 return g.group;
178 }
179 /**
180 * retrieves the vote items a client has been assigned
181 * @param client the client to retrieve
182 */
183 getVotesByClient(client) {
184 return this.getVotes().filter(item => item.claimedBy === client.uid());
185 }
186 /**
187 * gets all unclaimed votes a client nickname can be assigned to
188 * @param nick the nickname to check
189 */
190 getUnclaimedByNickname(nick) {
191 return this.getVotes()
192 .filter(item => this.isUnclaimed(item))
193 .filter(item => item.nickname === nick);
194 }
195 /**
196 * removes all items which are older than the days given in config
197 */
198 cleanOldItems() {
199 const votes = this.getVotes();
200 const cleaned = votes.filter(item => !this.isOld(item));
201 if (votes.length === cleaned.length)
202 return;
203 this.setVotes(cleaned);
204 }
205 /**
206 * interval checks
207 */
208 cron() {
209 this.cleanOldItems();
210 this.check();
211 }
212 /**
213 * sets an items status to claimed
214 * @param item the item which should get claimed
215 * @param uid the uid which claimed the item
216 */
217 flagItemClaimed(item, uid) {
218 item.claimedBy = uid;
219 item.claimedAt = Date.now();
220 return this;
221 }
222 /**
223 * creates a fully valid VoteItem
224 * @param item the item which should be upgraded
225 */
226 createVoteItem(item) {
227 return {
228 ...item,
229 added: Date.now(),
230 hash: this.getHash(item),
231 claimedBy: null,
232 claimedAt: 0
233 };
234 }
235 }
236 class TeamSpeakServers extends Vote {
237 constructor({ key, sid, createCommand }) {
238 super();
239 this.namespace = "teamspeakServersDotOrg_";
240 this.apikey = key;
241 this.sid = sid;
242 this.registerCommand(createCommand);
243 this.init();
244 }
245 registerCommand(createCommand) {
246 createCommand("vote")
247 .help("retrieves the vote link from teamspeak-servers.org")
248 .manual("retrieves the vote link for teamspeak-servers.org")
249 .manual(`vote daily to get rewarded with servergroups!`)
250 .exec((client, _, reply) => {
251 reply(`[b][url=https://teamspeak-servers.org/server/${this.sid}/vote/?username=${encodeURI(client.nick())}]VOTE HERE[/url]`);
252 reply(`It can take a few minutes until your vote gets counted!`);
253 if (removeAfter === -1) {
254 reply(`You have have voted ${this.getVotesByClient(client).length} times!`);
255 }
256 else {
257 reply(`You have have voted ${this.getVotesByClient(client).length} times in the last ${removeTime} days!`);
258 }
259 });
260 }
261 async check() {
262 const votes = await this.fetchVotes();
263 votes.forEach(vote => this.requestAdd({
264 nickname: vote.nickname,
265 timestamp: vote.timestamp
266 }));
267 }
268 fetchVotes() {
269 return new Promise((fulfill, reject) => {
270 http.simpleRequest({
271 method: "GET",
272 url: `https://teamspeak-servers.org/api/?object=servers&element=votes&key=${this.apikey}&format=json`
273 }, (err, res) => {
274 if (err)
275 return reject(new Error(`Failed to retrieve data from teamspeak-servers.org api! (Error ${err})`));
276 if (res.statusCode !== 200)
277 return reject(new Error(`Failed to retrieve data from teamspeak-servers.org api! (Code ${res.statusCode})`));
278 try {
279 fulfill(JSON.parse(res.data.toString()).votes);
280 }
281 catch (e) {
282 engine.log(`got response from teamspeak-servers.org: ${res.data.toString()}`);
283 return reject(e);
284 }
285 });
286 });
287 }
288 }
289 const votings = [];
290 function doGlobalCheck() {
291 votings.forEach(vote => vote.cron());
292 backend.getClients()
293 .filter(c => !c.isSelf())
294 .forEach(c => votings.forEach(v => v.checkClient(c)));
295 }
296 event.on("connect", () => {
297 if (initialized)
298 return;
299 initialized = true;
300 doGlobalCheck();
301 });
302 event.on("disconnect", () => {
303 initialized = false;
304 });
305 event.on("clientMove", ({ fromChannel, client }) => {
306 if (fromChannel || client.isSelf())
307 return;
308 votings.forEach(v => v.checkClient(client));
309 });
310 event.on("clientNick", client => votings.forEach(v => v.checkClient(client)));
311 event.on("serverGroupAdded", ev => votings.forEach(v => v.checkClient(ev.client)));
312 event.on("serverGroupRemoved", ev => votings.forEach(v => v.checkClient(ev.client)));
313 setInterval(() => {
314 if (!backend.isConnected())
315 return;
316 votings.forEach(vote => vote.cron());
317 }, CHECK_INTERVAL);
318 event.on("load", () => {
319 const command = require("command");
320 const { createCommand } = command;
321 votings.push(new TeamSpeakServers({ key, sid, createCommand }));
322 if (backend.isConnected()) {
323 initialized = true;
324 doGlobalCheck();
325 }
326 });
327});