This repository has been archived by the owner on Oct 11, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathformatting.js
427 lines (402 loc) · 16.1 KB
/
formatting.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/**
* Provides a number of functions for formatting text, cards and decks on Slack using the Slack API and Markdown.
* @module formating
* @author Dominic Shelton.
*/
'use strict';
/**
* @var {object} colours
* The faction colours are loaded from a JSON file.
*/
var colours = require('./colours.json');
/**
* @var {string[]|string[][]} packs
* The pack and cycle names are loaded from a JSON file.
*/
var packs = require('./datapacks.json');
/**
* @var {module} alliances
* The {@link alliances} module is required for calculating the decklist influence total.
*/
var alliances = require('./alliances.js');
/**
* @var {string} thumbsURL
* The URL where the card thumbnails are hosted, loaded from the environment variable THUMBS_URL
*/
var thumbsURL = process.env.THUMBS_URL;
/**
* @var {object} messages
* Various text messages conveniently combined into one object.
*/
var messages = {
noCardHits: [
"The run was successful but you didn't access _\u200b[cards]\u200b_.",
"I was unable to find _\u200b[cards]\u200b_ in any of my remotes.",
"Despite 822 Medium counters, _\u200b[cards]\u200b_ wasn't found.",
"The Near-Earth Hub couldn't locate _\u200b[cards]\u200b_."
],
noDeckHits: [
"The archetype of that deck would be _\u200bnon-existant\u200b_."
],
helpBrackets:
"Search for a card by (partial) name, or acronym, or a decklist by its netrunnerdb link e.g.\`\`\`[sneakdoor] [hiemdal] [etf]\n[netrunnerdb\u200b.com/en/decklist/17055/example]\`\`\`",
helpDeck:
"Search for a decklist by its netrunnerdb link or ID number e.g.\`\`\`[command] 12345, [command] netrunnerdb\u200b.com/en/decklist/17055/example\`\`\`",
helpCard:
"Search for a card by (partial) name, approximation or acronym e.g.\`\`\`[command]sneakdoor, [command]hiemdal, [command]etf\`\`\`",
unauthorized: "Unauthorized access detected.\n:_subroutine: End the run.\n:_subroutine: End the run.",
unavailable:
"The NetrunnerDB info is still being fetched, try again in a minute or two."
};
/**
* @var {string[][]} headings
* Headings used in Decklists, in the order they appear in the decklist output.
*/
var headings = [
['Event', 'Hardware', 'Resource', 'Agenda', 'Asset', 'Upgrade', 'Operation'],
['Icebreaker', 'Program', 'Barrier', 'Code Gate', 'Sentry', 'Multi', 'Other']
];
/**
* @var {string[][]} stats
* An array of pairs of card stats and corresponding emoji, in the order they should appear in card descriptions.
*/
var stats = [
['baselink', ' :_link:'],
['cost', ':_credit:'],
['rezcost', ':_rez:'],
['memoryunits', ' :_mu:'],
['strength', ' str'],
['trash', ' :_trash:'],
['advancementcost', ' :_advance:'],
['minimumdecksize', ' :_deck:'],
['influencelimit', '•'],
['agendapoints', ' :_agenda:']
];
/**
* @func cardHelpMessage
* @param command {string} The Slack command that was used to invoke the help request, used in the examples returned. Can be left blank to invoke the fallback brackets help text.
* @return {object} A Slack API message object of a help response message.
*/
module.exports.cardHelpMessage = cardHelpMessage;
function cardHelpMessage(command) {
if (command) {
return {
text: messages.helpCard.replace(/\[command\]/gi, command + ' ')
};
}
return {
text: messages.helpBrackets
};
};
/**
* @func deckHelpMessage
* @param command {string} The Slack command that was used to invoke the help request, used in the examples returned.
* @return {object} A Slack API message object of a help response message.
*/
module.exports.deckHelpMessage = deckHelpMessage;
function deckHelpMessage(command) {
return {
text: messages.helpDeck.replace(/\[command\]/gi, command)
};
};
/**
* Returns a message to the user that the nrdb database is not yet loaded
* @return {string} A private message to the user stating that nrdb is not yet loaded
*/
module.exports.unauthorizedMessage = unauthorizedMessage;
function unauthorizedMessage() {
return {
"text": messages.unauthorized
}
}
/**
* Returns a message to the user that the nrdb database is not yet loaded
* @return {string} A private message to the user stating that nrdb is not yet loaded
*/
module.exports.unavailableMessage = unavailableMessage;
function unavailableMessage() {
return {
"response_type": "ephemeral",
"text": messages.unavailable
}
}
/**
* Generates a message indicating that cards weren't found, by concatenating the names with commas and 'or'
* @func cardNoHitsMessage
* @param cards {string[]} An array of card titles that weren't found.
* @return {string} A randomized Slack API message object stating that the given cards weren't found.
*/
module.exports.cardNoHitsMessage = cardNoHitsMessage;
function cardNoHitsMessage(cards) {
var text;
if (cards.length >= 2) {
text = cards.slice(0, cards.length - 1).join(', ');
text += ' or ' + cards[cards.length - 1];
} else {
text = cards[0];
}
var message = getCardNoHitsMessage(text);
return {
text: message
};
};
/**
* Generates a message indicating that a deck wasn't found.
* @func deckNoHitsMessage
* @return {string} A randomized Slack API message object stating that the deck wasn't found.
*/
module.exports.deckNoHitsMessage = deckNoHitsMessage;
function deckNoHitsMessage() {
var r = Math.floor(Math.random() * messages.noDeckHits.length);
return {
text: messages.noDeckHits[r]
};
};
/**
* Applies Slack markdown formatting to the given title to bolden and optionally apply the link.
* @func formatTitle
* @param title {string} The text to display as the title.
* @param [url] {string} The URL to link the title to.
* @return {string} A string containing the title with Slack markdown formatting applied.
*/
module.exports.formatTitle = formatTitle;
function formatTitle(title, url) {
title = '*\u200b' + title + '\u200b*';
if (url && url !== '') {
return formatLink(title, url);
}
return title;
};
/**
* @func formatDecklist
* @param decklist {object} The decklist to be converted into a Slack message.
* @return {object} A Slack API message object containing the decklist or an error message.
*/
module.exports.formatDecklist = (decklist) => {
// Initialise the return object.
var o = {text: '', attachments:[{mrkdwn_in: ['pretext', 'fields']}]};
var faction = decklist.cards.Identity[0].card.faction;
var usedInfluence = 0;
var mwlDeduction = 0;
var decksize = 0;
var agendapoints = 0;
var fields = [];
// Initialise the newestCard var to a card that is guaranteed to be in every deck.
var newestCard = parseInt(decklist.cards.Identity[0].card.code);
o.text = formatTitle(decklist.name, decklist.url);
if (decklist.privateDeck) {
o.text += ' _\u200b(private)\u200b_';
}
o.text += ' - _\u200b' + decklist.creator + '\u200b_';
for (let column in headings) {
// Create the columns as Slack 'fields'.
fields[column] = {title: '', value: '', short: true};
for (let heading in headings[column]) {
var type = headings[column][heading];
// Check if the deck actually contains cards of the specified heading
if (decklist.cards[type]) {
var typeTotal = 0;
var text = '';
// Iterate through all the cards of the type in the decklist.
for (let i in decklist.cards[type]) {
var card = decklist.cards[type][i];
var code = parseInt(card.card.code);
typeTotal += card.quantity;
text += '\n' + card.quantity;
text += ' × ' + formatLink(card.card.title, card.card.url);
decksize += card.quantity;
// Check that the card is not newer than the previous newest card.
if (code > newestCard) {
newestCard = code;
}
if (card.card.agendapoints) {
agendapoints += card.card.agendapoints * card.quantity;
}
// Add MWL star if required.
if (card.card.mwl)
{
var mwl = card.quantity * card.card.mwl;
text += ' ' + '☆'.repeat(mwl);
mwlDeduction += mwl;
}
// Add influence dots after the card name if required.
if (card.card.faction !== faction) {
var inf = card.quantity * card.card.factioncost;
if (alliances[card.card.code]) {
inf *= alliances[card.card.code](decklist);
}
text += ' ' + influenceDots(inf);
usedInfluence += inf;
}
}
// If this is not the first heading, add padding after the previous.
if (heading != 0) {
fields[column].value += '\n\n';
}
fields[column].value += formatTitle(type) + ' (' + typeTotal + ')';
fields[column].value += text;
}
}
}
o.attachments[0].color = colours[faction];
o.attachments[0].fields = fields;
// The identity of the decklist is displayed before the decklist.
o.attachments[0].pretext = formatLink(decklist.cards.Identity[0].card.title,
decklist.cards.Identity[0].card.url);
o.attachments[0].pretext += '\n' + decksize + ' :_deck: (min ';
o.attachments[0].pretext += decklist.cards.Identity[0].card.minimumdecksize;
o.attachments[0].pretext += ') - ' + usedInfluence + '/';
if (decklist.cards.Identity[0].card.influencelimit) {
o.attachments[0].pretext += decklist.cards.Identity[0].card.influencelimit - mwlDeduction + '•';
if (mwlDeduction > 0) {
o.attachments[0].pretext += '(' + decklist.cards.Identity[0].card.influencelimit + '-';
o.attachments[0].pretext += mwlDeduction + '☆)';
}
} else {
o.attachments[0].pretext += '∞•';
}
if (decklist.cards.Identity[0].card.side !== 'Runner') {
o.attachments[0].pretext += ' - ' + agendapoints + ' :_agenda:';
}
o.attachments[0].pretext += '\nCards up to ' + getPack(newestCard);
return o;
};
/**
* @func formatCards
* @param [cards] {object[]} The cards to be converted into a Slack message.
* @param [cards] {object[]} The cards that weren't found and should be mentioned in an error message.
* @return {object} A Slack API Message object either containing the cards as attachments or containing an error message that the cards couldn't be found, or both.
*/
module.exports.formatCards = (cards, missing) => {
var o;
// If there are cards that could not be found, tell the user.
if (missing && missing.length > 0) {
o = cardNoHitsMessage(missing);
o.attachments = [];
} else {
o = {text:'', attachments:[]};
}
// Display the cards that were found either way.
for (var i = 0; i < cards.length; i++) {
var a = {pretext: '', mrkdwn_in: ['pretext', 'text']};
var faction = cards[i].faction;
var title = cards[i].title;
if (cards[i].uniqueness){
title = '◆ ' + title;
}
// If the Slack message is blank, put the title of the first card there.
// The Slack API won't display messages with blank text even when there are attachments.
if (o.text === '') {
o.text = formatTitle(title, cards[0].url);
} else {
a.pretext = formatTitle(title, cards[i].url) + '\n';
}
// Append the rest of the card details to the attachment.
a.pretext += '*\u200b' + cards[i].type;
if (cards[i].subtype ) {
a.pretext += ':\u200b* ' + cards[i].subtype;
} else {
a.pretext += '\u200b*';
}
a.pretext += ' - ' + getFactionEmoji(faction);
if (cards[i].factioncost) {
a.pretext += influenceDots(cards[i].factioncost);
}
if (cards[i].mwl) {
a.pretext += '☆'.repeat(cards[i].mwl);
}
a.pretext += '\n';
var first = true;
// Iterate through the possible card stats adding them to the text where present.
for (var j = 0; j < stats.length; j++) {
if (cards[i][stats[j][0]] || cards[i][stats[j][0]] === 0) {
if (!first) {
a.pretext += ' - ';
}
a.pretext += cards[i][stats[j][0]] + stats[j][1];
first = false;
// Special case for draft IDs with infinite influence limit
} else if (cards[i].type === 'Identity' && stats[j][0] === 'influencelimit' && !cards[i].influencelimit) {
a.pretext += ' - ∞•';
}
}
// Replace memory units with the slack emoji
a.pretext = a.pretext.replace(/(\d|X)\s*:_mu:/gi, function (x) {
return x.replace(/(.).*/, ':_$1mu:').toLowerCase();
});
a.pretext += ' - ' + getPack(parseInt(cards[i].code));
a.color = colours[faction];
// Add the card text to the attachment if present.
if (cards[i].text) {
a.text = formatText(cards[i].text);
}
a.thumb_url = thumbsURL + cards[i].code + '.png';
o.attachments.push(a);
}
return o;
};
/**
* Replace all the html and nrdb text with the Slack equivalent.
* @func formatText
* @param text {string} The body of text to convert to Slack formatting.
* @return {string} The text, formatted in Slack markdown syntax.
*/
module.exports.formatText = formatText;
function formatText(text) {
if (!text) return text;
text = text.replace(/\r\n/g, '\n');
// NRDB symbols to Slack emoji.
text = text.replace(/\[credits?\]/gi, ':_credit:');
text = text.replace(/\[recurring[\- ]credits?\]/gi, ':_recurringcredit:');
text = text.replace(/\[click\]/gi, ':_click:');
text = text.replace(/\ *\[link\]/gi, ' :_link:');
text = text.replace(/\[trash\]/gi, ':_trash:');
text = text.replace(/\[subroutine\]/gi, ':_subroutine:');
// Individual mu emoji for numbers and 'Xmu'
text = text.replace(/(\d|X)\s*\[(?:memory unit|mu)\]/gi, function (x) {
return x.replace(/(.).*/, ':_$1mu:').toLowerCase();
});
text = text.replace(/\[(?:memory unit|mu)\]/gi, ':_mu:');
// HTML bold to Slack bold
text = text.replace(/<strong>/gi, '*\u200b');
text = text.replace(/<\/strong>/gi, '\u200b*');
// Errata tags
text = text.replace(/<errata>/gi, ':_exclamation: _\u200b');
text = text.replace(/<\/errata>/gi, '\u200b_');
// Convert traces into unicode superscripts and format accordingly
text = text.replace(/<trace>(trace)\s*(\d+|X)<\/trace>/gi, function (a, x, y){
y = y.replace(/X/i,'ˣ');
y = y.replace(/\d/g, function (d){
return ['⁰','¹','²','³','⁴','⁵','⁶','⁷','⁸','⁹'][parseInt(d)];
});
return '*\u200b' + x + y + '\u200b*\u2013';
});
// Replace nrdb faction symbols with Slack emoji
text = text.replace(/\[(jinteki|weyland-consortium|nbn|haas-bioroid|anarch|shaper|criminal)\]/, (a, x) => {
return getFactionEmoji(x);
});
text = text.replace(/&/g, '&');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
return text;
};
function influenceDots(influence) {
return '•'.repeat(influence);
}
function getPack(code) {
var cycle = packs[Math.floor(code/1000)];
if (Array.isArray(cycle)) {
cycle = cycle[Math.floor(((code % 1000) - 1) / 20)];
}
return '_\u200b' + cycle + '\u200b_';
}
function formatLink(text, url) {
return '<' + url + '|' + text + '>';
}
function getFactionEmoji(faction) {
return ':_' + faction.replace(/[\s-].*/, '').toLowerCase() + ':';
}
function getCardNoHitsMessage(text) {
var r = Math.floor(Math.random() * messages.noCardHits.length);
return messages.noCardHits[r].replace(/\[cards\]/gi, text);
}