Dernière activité 1778885832

Fogest a révisé ce gist 1778885832. Aller à la révision

1 file changed, 486 insertions

batch balance (fixed) (fichier créé)

@@ -0,0 +1,486 @@
1 + // ==UserScript==
2 + // @name Batch Balance
3 + // @namespace https://github.com/tobytorn
4 + // @description Distribute money or change balances for multiple faction members (Not supported on mobile)
5 + // @author tobytorn [1617955]
6 + // @match https://www.torn.com/factions.php?step=your*
7 + // @version 2.0.2
8 + // @grant GM_getValue
9 + // @grant GM_setValue
10 + // @grant GM_addStyle
11 + // @supportURL https://github.com/tobytorn/batch-balance
12 + // @license MIT
13 + // @require https://unpkg.com/[email protected]/dist/jquery.min.js
14 + // @downloadURL https://update.greasyfork.org/scripts/536376/Batch%20Balance.user.js
15 + // @updateURL https://update.greasyfork.org/scripts/536376/Batch%20Balance.meta.js
16 + // ==/UserScript==
17 +
18 + // Usage:
19 + // Add the following parameters to the URL of the control page (https://www.torn.com/factions.php?step=your#/tab=controls) to enable this script
20 + // batbal_uids Comma-separated user IDs
21 + // batbal_amounts Comma-separated amounts
22 + // batbal_action [Optional] "add" for adding to balance (default), or "give" for giving money
23 + // batbal_asset [Optional] "money" (default) or "points"
24 + //
25 + // Example: The following URL will add 120 to Leslie, subtract 250 from tobytorn, and add 1.5k to Duke
26 + // https://www.torn.com/factions.php?step=your#/tab=controls&batbal_uids=15,1617955,4&batbal_amounts=120,-250,1500
27 +
28 + 'use strict';
29 +
30 + function batchBalanceWrapper() {
31 + console.log('Batch Balance starts');
32 +
33 + const ACTION_INTERVAL_MS = 1000;
34 + const GM_VALUE_KEY = 'batbal-action';
35 + const PROFILE_HREF_PREFIX = 'profiles.php?XID=';
36 + const ACTION_SPECS = {
37 + give: {
38 + summary: 'Give',
39 + text: 'Give',
40 + waitingText: 'Giving',
41 + bodyParam: 'giveMoney',
42 + },
43 + add: {
44 + summary: 'Add to balance',
45 + text: 'Add',
46 + waitingText: 'Adding',
47 + bodyParam: 'addToBalance',
48 + },
49 + };
50 +
51 + const $ = window.jQuery;
52 +
53 + const LOCAL_STORAGE_PREFIX = 'BATCH_BALANCE_';
54 +
55 + function getLocalStorage(key, defaultValue) {
56 + const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key);
57 + try {
58 + return JSON.parse(value) ?? defaultValue;
59 + } catch (err) {
60 + return defaultValue;
61 + }
62 + }
63 +
64 + function setLocalStorage(key, value) {
65 + window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value));
66 + }
67 +
68 + const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda');
69 + const [getValue, setValue] =
70 + isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function'
71 + ? [getLocalStorage, setLocalStorage]
72 + : [window.GM_getValue, window.GM_setValue];
73 +
74 + const STYLE = `
75 + .batbal-overlay {
76 + position: relative;
77 + }
78 + .batbal-overlay:after {
79 + content: '';
80 + position: absolute;
81 + background: repeating-linear-gradient(135deg, #2228, #2228 70px, #0008 70px, #0008 80px);
82 + top: 0;
83 + left: 0;
84 + width: 100%;
85 + height: 100%;
86 + z-index: 900000;
87 + }
88 + #batbal-ctrl {
89 + margin: 10px 0;
90 + padding: 10px;
91 + border-radius: 5px;
92 + background-color: var(--default-bg-panel-color);
93 + text-align: center;
94 + line-height: 16px;
95 + }
96 + #batbal-ctrl-detail > :not(:first-child),
97 + #batbal-ctrl > :not(:first-child) {
98 + margin-top: 10px;
99 + }
100 + #batbal-ctrl-title {
101 + font-size: large;
102 + font-weight: bold;
103 + }
104 + #batbal-ctrl-status {
105 + font-weight: bold;
106 + }
107 + #batbal-ctrl button {
108 + margin: 0 4px;
109 + }
110 + #batbal-ctrl table {
111 + margin: 0 auto;
112 + }
113 + #batbal-ctrl th {
114 + font-weight: bold;
115 + }
116 + #batbal-ctrl th,
117 + #batbal-ctrl td {
118 + color: inherit;
119 + padding: 5px;
120 + border: 1px solid #ccc;
121 + }
122 + #batbal-ctrl td:last-child {
123 + text-align: right;
124 + }
125 + #batbal-ctrl-detail tr.batbal-done:after {
126 + content: '\u2713';
127 + color: green;
128 + padding-left: 6px;
129 + }
130 + `;
131 +
132 + const CONTROLLER_HTML = `
133 + <div id="batbal-ctrl">
134 + <div id="batbal-ctrl-title">Batch Balance</div>
135 + <div>
136 + <table>
137 + <thead>
138 + <tr>
139 + <th colspan="2">Summary</th>
140 + </tr>
141 + </thead>
142 + <tbody>
143 + <tr>
144 + <th>Action</th>
145 + <td id="batbal-ctrl-summary-action-type">-</td>
146 + </tr>
147 + <tr>
148 + <th>Asset Type</th>
149 + <td id="batbal-ctrl-summary-asset-type">-</td>
150 + </tr>
151 + <tr>
152 + <th>Player Count</th>
153 + <td id="batbal-ctrl-summary-player-count">-</td>
154 + </tr>
155 + <tr>
156 + <th>Player Not in Faction</th>
157 + <td><span id="batbal-ctrl-summary-player-not-in-faction">-</span></td>
158 + </tr>
159 + <tr>
160 + <th>Total Amount</th>
161 + <td><span id="batbal-ctrl-summary-total-amount">-</span></td>
162 + </tr>
163 + </tbody>
164 + </table>
165 + </div>
166 + <div>
167 + <button id="batbal-ctrl-start" class="torn-btn" disabled>Start</button>
168 + <button id="batbal-ctrl-show-detail" class="torn-btn">Show details</button>
169 + <button id="batbal-ctrl-hide-detail" class="torn-btn" style="display: none">Hide details</button>
170 + <button id="batbal-ctrl-clear-data" class="torn-btn" disabled>Clear data</button>
171 + </div>
172 + <button id="batbal-ctrl-submit" class="torn-btn" style="display: none" disabled></button>
173 + <div>Status: <span id="batbal-ctrl-status"></span></div>
174 + <div id="batbal-ctrl-detail" style="display: none">
175 + <table>
176 + <thead>
177 + <tr>
178 + <th>ID</th>
179 + <th>Name</th>
180 + <th>Amount</th>
181 + <th>Note</th>
182 + </tr>
183 + </thead>
184 + <tbody></tbody>
185 + </table>
186 + </div>
187 + </div>`;
188 +
189 + function formatAmount(v) {
190 + return (v >= 0 ? '+' : '') + v.toString().replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => s + ',');
191 + }
192 +
193 + async function sleep(t) {
194 + await new Promise((r) => setTimeout(r, t));
195 + }
196 +
197 + // Copied from https://stackoverflow.com/a/25490531
198 + function getCookie(name) {
199 + return document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
200 + }
201 +
202 + function getParams() {
203 + const params = new URLSearchParams(location.hash.slice(1));
204 + return params.get('/tab') === 'controls' ? params : null;
205 + }
206 +
207 + function storeAction(action) {
208 + setValue(GM_VALUE_KEY, action);
209 + }
210 +
211 + function parseAction() {
212 + const params = getParams();
213 + if (!params) {
214 + return null;
215 + }
216 + const paramUids = params.get('batbal_uids');
217 + const paramAmounts = params.get('batbal_amounts');
218 + if (paramUids === null || paramAmounts === null) {
219 + return null;
220 + }
221 + const uids = paramUids.split(',');
222 + const amounts = paramAmounts.split(',');
223 + if (amounts.length !== uids.length) {
224 + return { error: 'Param "batbal_uids" and "batbal_amounts" have different lengths' };
225 + }
226 + if (uids.length === 0 || uids.some((x) => !x.match(/^\d+$/))) {
227 + return { error: 'Param "batbal_uids" is invalid' };
228 + }
229 + if (amounts.length === 0 || amounts.some((x) => !x.match(/^[+-]?\d{1,11}$/))) {
230 + return { error: 'Param "batbal_amounts" is invalid' };
231 + }
232 + const actionType = params.get('batbal_action') ?? 'add';
233 + if (!['give', 'add'].includes(actionType)) {
234 + return { error: 'Param "batbal_action" is invalid' };
235 + }
236 + const assetType = params.get('batbal_asset') ?? 'money';
237 + if (!['money', 'points'].includes(assetType)) {
238 + return { error: 'Param "batbal_asset" is invalid' };
239 + }
240 + return {
241 + uidAmounts: uids.map((uid, i) => [uid, parseInt(amounts[i])]).filter(([, amount]) => amount !== 0),
242 + next: 0,
243 + actionType,
244 + assetType,
245 + };
246 + }
247 +
248 + function checkAction(parsedAction, storedAction) {
249 + if (parsedAction && storedAction) {
250 + if (
251 + JSON.stringify(parsedAction.uidAmounts) !== JSON.stringify(storedAction.uidAmounts) ||
252 + parsedAction.actionType !== storedAction.actionType ||
253 + parsedAction.assetType !== storedAction.assetType
254 + ) {
255 + throw new Error(
256 + "An unfinished Batch Balance operation was found that doesn't match the URL parameters. " +
257 + 'Click "Show details" to view the pending operation. ' +
258 + 'To resume it, clear the URL parameters and refresh the page.' +
259 + 'To discard it, click "Clear data" and refresh the page.',
260 + );
261 + }
262 + }
263 + return storedAction ?? parsedAction;
264 + }
265 +
266 + /** @returns Promise<Record<string, { name: string, isInFaction: boolean }>> */
267 + async function getUidMap() {
268 + return new Promise((resolve) => {
269 + const interval = setInterval(function () {
270 + const $depositors = $('.money___DbkSX .userListWrap___AndUA .userInfoWrap___k2sjQ');
271 + if ($depositors.length === 0) {
272 + return;
273 + }
274 + const map = {};
275 + $depositors.each(function () {
276 + const $name = $(this).find(`a[href*="${PROFILE_HREF_PREFIX}"]`).first();
277 + if ($name.length > 0) {
278 + const uid = ($name.attr('href') || '').split(PROFILE_HREF_PREFIX)[1];
279 + map[uid] = {
280 + name: $name.text().trim(),
281 + isInFaction: !$(this).hasClass('inactive___Hd0EQ'),
282 + };
283 + }
284 + });
285 + clearInterval(interval);
286 + resolve(map);
287 + }, 200);
288 + });
289 + }
290 +
291 + function renderController() {
292 + GM_addStyle(STYLE);
293 + const $controlsWrap = $('.faction-controls-wrap');
294 + $controlsWrap.addClass('batbal-overlay');
295 + $controlsWrap.before(CONTROLLER_HTML);
296 + $('#batbal-ctrl-show-detail').on('click', function () {
297 + $('#batbal-ctrl-detail').show();
298 + $('#batbal-ctrl-hide-detail').show();
299 + $(this).hide();
300 + });
301 + $('#batbal-ctrl-hide-detail').on('click', function () {
302 + $('#batbal-ctrl-detail').hide();
303 + $('#batbal-ctrl-show-detail').show();
304 + $(this).hide();
305 + });
306 + $('#batbal-ctrl-clear-data').on('click', function () {
307 + if (
308 + confirm(
309 + 'Are you sure you want to delete the saved Batch Balance data? ' +
310 + 'This will remove any unfinished operations and cannot be undone.',
311 + )
312 + ) {
313 + storeAction(null);
314 + $('#batbal-ctrl').hide();
315 + alert('Saved data has been deleted, please refresh the page');
316 + }
317 + });
318 + }
319 +
320 + function updateStatus(s) {
321 + $('#batbal-ctrl-status').text(String(s));
322 + if (s instanceof Error) {
323 + $('#batbal-ctrl-status').css('color', 'red');
324 + }
325 + }
326 +
327 + function renderDetails(action, uidMap) {
328 + const $tbody = $('#batbal-ctrl-detail tbody');
329 + $tbody.empty();
330 + let outsideCount = 0;
331 + action.uidAmounts.forEach(([uid, amount], i) => {
332 + const amountClass = amount >= 0 ? 't-green' : 't-red';
333 + const trClass = i < action.next ? 'batbal-done' : '';
334 + const uidInfo = uidMap[uid] || {};
335 + const name = uidInfo.name || '';
336 + const isInFaction = uidInfo.isInFaction || false;
337 + if (!isInFaction) {
338 + outsideCount++;
339 + }
340 + $tbody.append(`<tr class="${trClass}">
341 + <td>${uid}</td>
342 + <td>${name}</td>
343 + <td><span class="${amountClass}">${formatAmount(amount)}</span></td>
344 + <td><span class="${!isInFaction ? 't-red' : ''}">${!isInFaction ? 'Not in faction' : ''}</span></td>
345 + </tr>`);
346 + });
347 + const total = action.uidAmounts.reduce((v, [, amount]) => v + amount, 0);
348 + const totalClass = total >= 0 ? 't-green' : 't-red';
349 + const actionSpec = ACTION_SPECS[action.actionType];
350 + $('#batbal-ctrl-summary-action-type').text(actionSpec.summary);
351 + $('#batbal-ctrl-summary-asset-type').text(action.assetType);
352 + $('#batbal-ctrl-summary-player-count').text(action.uidAmounts.length);
353 + $('#batbal-ctrl-summary-player-not-in-faction')
354 + .text(outsideCount)
355 + .toggleClass('t-red', outsideCount > 0);
356 + $('#batbal-ctrl-summary-total-amount').text(formatAmount(total)).addClass(totalClass);
357 + updateStatus(`Progress: ${action.next} / ${action.uidAmounts.length} done`);
358 + }
359 +
360 + async function addMoney({ uid, name, amount, actionType, assetType }) {
361 + const $submit = $('#batbal-ctrl-submit');
362 + $submit.show();
363 + const actionSpec = ACTION_SPECS[actionType];
364 + const textSuffix = ` ${assetType}: ${name} [${uid}] ${formatAmount(amount)}`;
365 + const queryParam = {
366 + money: 'factionsGiveMoney',
367 + points: 'factionsGivePoints',
368 + }[assetType];
369 + $submit.text(`${actionSpec.text} ${textSuffix}`);
370 + $submit.prop('disabled', false);
371 + return new Promise((resolve, reject) => {
372 + $submit.on('click', async () => {
373 + try {
374 + $submit.off('click');
375 + $submit.text(`${actionSpec.waitingText} ${textSuffix}`);
376 + $submit.prop('disabled', true);
377 + const rfcv = getCookie('rfc_v');
378 + const rsp = await fetch(`/page.php?sid=${queryParam}&rfcv=${rfcv}`, {
379 + method: 'POST',
380 + headers: {
381 + 'Content-Type': 'application/json',
382 + 'x-requested-with': 'XMLHttpRequest',
383 + },
384 + body: JSON.stringify({
385 + option: actionSpec.bodyParam,
386 + receiver: parseInt(uid),
387 + amount,
388 + }),
389 + });
390 + const rawData = await rsp.text();
391 + if (!rsp.ok) {
392 + throw new Error(`Network error: ${rsp.status} ${rawData}`);
393 + }
394 + const data = JSON.parse(rawData);
395 + if (data.success === true) {
396 + resolve();
397 + } else {
398 + reject(new Error(`Unexpected server response: ${rawData}`));
399 + }
400 + } catch (err) {
401 + reject(err);
402 + }
403 + });
404 + });
405 + }
406 +
407 + async function start(action, uidMap) {
408 + storeAction(action);
409 + $('#batbal-ctrl-start').prop('disabled', true);
410 + $('#batbal-ctrl-clear-data').prop('disabled', true);
411 +
412 + try {
413 + while (action.next < action.uidAmounts.length) {
414 + updateStatus(`Current progress: ${action.next} / ${action.uidAmounts.length} done`);
415 + const now = Date.now();
416 + const [uid, amount] = action.uidAmounts[action.next];
417 + const uidInfo = uidMap[uid] || {};
418 + const name = uidInfo.name || 'Unknown player';
419 + await addMoney({ uid, name, amount, actionType: action.actionType, assetType: action.assetType });
420 + action.next++;
421 + storeAction(action);
422 + renderDetails(action, uidMap);
423 + const elapsed = Date.now() - now;
424 + if (elapsed < ACTION_INTERVAL_MS) {
425 + await sleep(ACTION_INTERVAL_MS - elapsed);
426 + }
427 + }
428 + storeAction(null);
429 + updateStatus('All done!');
430 + } catch (err) {
431 + updateStatus(err);
432 + }
433 + }
434 +
435 + async function main() {
436 + try {
437 + const parsedAction = parseAction();
438 + const storedAction = getValue(GM_VALUE_KEY, null);
439 + if (storedAction === null && parsedAction === null) {
440 + return;
441 + }
442 +
443 + const uidMap = await getUidMap();
444 + renderController();
445 + if (storedAction) {
446 + renderDetails(storedAction, uidMap);
447 + $('#batbal-ctrl-clear-data').prop('disabled', false);
448 + }
449 + if (parsedAction.error) {
450 + throw new Error(parsedAction.error);
451 + }
452 +
453 + const action = checkAction(parsedAction, storedAction);
454 + if (!storedAction) {
455 + renderDetails(action, uidMap);
456 + }
457 + if (action.actionType === 'give') {
458 + if (action.uidAmounts.some(([uid]) => !uidMap[uid]?.isInFaction)) {
459 + throw new Error('Some players are not in the faction');
460 + }
461 + if (action.uidAmounts.some(([, amount]) => amount <= 0)) {
462 + throw new Error('Amounts to give must be positive');
463 + }
464 + }
465 +
466 + $('#batbal-ctrl-start').prop('disabled', false);
467 + $('#batbal-ctrl-start').on('click', () => start(action, uidMap));
468 + } catch (err) {
469 + updateStatus(err);
470 + console.log('Unhandled exception from Batch Balance:', err);
471 + }
472 + }
473 +
474 + main();
475 + console.log('Batch Balance ends');
476 + }
477 +
478 + if (document.readyState === 'loading') {
479 + document.addEventListener('readystatechange', () => {
480 + if (document.readyState === 'interactive') {
481 + batchBalanceWrapper();
482 + }
483 + });
484 + } else {
485 + batchBalanceWrapper();
486 + }
Plus récent Plus ancien