blob: 9b1cd9aae7655673f84d41db1cf2119ffae7ec19 [file] [log] [blame]
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +01001import {MDCTooltip} from '@material/tooltip';
2import {waitFor} from 'poll-until-promise';
3
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +01004import {parseUrl} from '../../common/commonUtils.js';
Adrià Vilanova Martínez17106dc2022-01-24 23:48:36 +01005import OptionsWatcher from '../../common/optionsWatcher.js';
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +01006import {createPlainTooltip} from '../../common/tooltip.js';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +01007
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +01008import {createExtBadge, getDisplayLanguage} from './utils/common.js';
Adrià Vilanova Martínez69c30502022-01-28 20:47:08 +01009import PerForumStatsSection from './utils/PerForumStatsSection.js';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010010
11const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +010012const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +010013const kViewThreadResponse = 'TWPT_ViewThreadResponse';
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +010014const kViewForumResponse = 'TWPT_ViewForumResponse';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010015
16const kAbuseCategories = [
17 ['1', 'Account'],
18 ['2', 'Display name'],
19 ['3', 'Avatar'],
20];
21const kAbuseViolationCategories = {
22 0: 'NO_VIOLATION',
23 1: 'COMMUNITY_POLICY_VIOLATION',
24 2: 'LEGAL_VIOLATION',
25 3: 'CSAI_VIOLATION',
26 4: 'OTHER_VIOLATION',
27};
28const kAbuseViolationTypes = {
29 0: 'UNSPECIFIED',
30 23: 'ACCOUNT_DISABLED',
31 55: 'ACCOUNT_HAS_SERVICES_DISABLED',
32 35: 'ACCOUNT_HIJACKED',
33 96: 'ACCOUNT_LEAKED_CREDENTIALS',
34 92: 'ACCOUNT_NOT_SUPPORTED',
35 81: 'ARTISTIC_NUDITY',
36 66: 'BAD_BEHAVIOR_PATTERN',
37 78: 'BAD_ENGAGEMENT_BEHAVIOR_PATTERN',
38 79: 'BORDERLINE_HARASSMENT',
39 80: 'BORDERLINE_HATE_SPEECH',
40 38: 'BOTNET',
41 32: 'BRANDING_VIOLATION',
42 100: 'CAPITALIZING_TRAGIC_EVENTS',
43 105: 'CLOAKING',
44 49: 'COIN_MINING',
45 7: 'COMMERCIAL_CONTENT',
46 97: 'COPPA_REGULATED',
47 57: 'COPYRIGHT_CIRCUMVENTION',
48 8: 'COPYRIGHTED_CONTENT',
49 58: 'COURT_ORDER',
50 51: 'CSAI',
51 94: 'CSAI_INSPECT',
52 52: 'CSAI_CARTOON_HUMOR',
53 53: 'CSAI_SOLICITATION',
54 108: 'CSAI_NON_APPARENT',
55 67: 'DANGEROUS',
56 37: 'DATA_SCRAPING',
57 86: 'DECEPTIVE_OAUTH_IMPLEMENTATION',
58 46: 'DEFAMATORY_CONTENT',
59 36: 'DELINQUENT_BILLING',
60 30: 'DISRUPTION_ATTEMPT',
61 112: 'DOMESTIC_INTERFERENCE',
62 22: 'DOS',
63 9: 'DUPLICATE_CONTENT',
64 68: 'DUPLICATE_LOCAL_PAGE',
65 121: 'NON_QUALIFYING_ORGANIZATION',
66 115: 'EGREGIOUS_INTERACTION_WITH_MINOR',
67 83: 'ENGAGEMENT_COLLUSION',
68 41: 'EXPLOIT_ATTACKS',
69 65: 'FAKE_USER',
70 2: 'FRAUD',
71 21: 'FREE_TRIAL_VIOLATION',
72 43: 'GIBBERISH',
73 101: 'FOREIGN_INTERFERENCE',
74 59: 'GOVERNMENT_ORDER',
75 10: 'GRAPHICAL_VIOLENCE',
76 11: 'HARASSMENT',
77 12: 'HATE_SPEECH',
78 90: 'IDENTICAL_PRODUCT_NAME',
79 60: 'ILLEGAL_DRUGS',
80 13: 'IMPERSONATION',
81 69: 'IMPERSONATION_WITH_PII',
82 116: 'INAPPROPRIATE_INTERACTION_WITH_MINOR',
83 45: 'INAPPROPRIATE_CONTENT_SPEECH',
84 106: 'INTENTIONAL_THWARTING',
85 27: 'INTRUSION_ATTEMPT',
86 87: 'INVALID_API_USAGE',
87 14: 'INVALID_CONTENT',
88 20: 'INVALID_GCE_USAGE',
89 120: 'INVALID_STORAGE_USAGE',
90 15: 'INVALID_IMAGE_QUALITY',
91 88: 'INVALID_API_PRIVACY_POLICY_DISCLOSURE',
92 54: 'INVALID_USAGE_OF_IP_PROXYING',
93 99: 'KEYWORD_STUFFING',
94 61: 'LEGAL_COUNTERFEIT',
95 62: 'LEGAL_EXPORT',
96 63: 'LEGAL_PRIVACY',
97 33: 'LEGAL_REVIEW',
98 91: 'LEGAL_PROTECTED',
99 70: 'LOW_QUALITY_CONTENT',
100 93: 'LOW_REPUTATION_PHONE_NUMBER',
101 6: 'MALICIOUS_SOFTWARE',
102 40: 'MALWARE',
103 113: 'MISLEADING',
104 114: 'MISREP_OF_ID',
105 89: 'MEMBER_OF_ABUSIVE_GCE_NETWORK',
106 84: 'NON_CONSENSUAL_EXPLICIT_IMAGERY',
107 1: 'NONE',
108 102: 'OFF_TOPIC',
109 31: 'OPEN_PROXY',
110 28: 'PAYMENT_FRAUD',
111 16: 'PEDOPHILIA',
112 71: 'PERSONAL_INFORMATION_CONTENT',
113 25: 'PHISHING',
114 34: 'POLICY_REVIEW',
115 17: 'PORNOGRAPHY',
116 29: 'QUOTA_CIRCUMVENTION',
117 72: 'QUOTA_EXCEEDED',
118 73: 'REGULATED',
119 24: 'REPEATED_POLICY_VIOLATION',
120 104: 'RESOURCE_COMPROMISED',
121 107: 'REWARD_PROGRAMS_ABUSE',
122 74: 'ROGUE_PHARMA',
123 82: 'ESCORT',
124 75: 'SPAMMY_LOCAL_VERTICAL',
125 39: 'SEND_EMAIL_SPAM',
126 117: 'SEXTORTION',
127 118: 'SEX_TRAFFICKING',
128 44: 'SEXUALLY_EXPLICIT_CONTENT',
129 3: 'SHARDING',
130 95: 'SOCIAL_ENGINEERING',
131 109: 'SUSPICIOUS',
132 19: 'TRADEMARK_CONTENT',
133 50: 'TRAFFIC_PUMPING',
134 76: 'UNSAFE_RACY',
135 103: 'UNUSUAL_ACTIVITY_ALERT',
136 64: 'UNWANTED_CONTENT',
137 26: 'UNWANTED_SOFTWARE',
138 77: 'VIOLENT_EXTREMISM',
139 119: 'UNAUTH_IMAGES_OF_MINORS',
140 85: 'UNAUTHORIZED_SERVICE_RESELLING',
141 98: 'CSAI_EXTERNAL',
142 5: 'SPAM',
143 4: 'UNSAFE',
144 47: 'CHILD_PORNOGRAPHY_INCITATION',
145 18: 'TERRORISM_SUPPORT',
146 56: 'CSAI_WORST_OF_WORST',
147};
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100148const kItemMetadataState = {
149 0: 'UNDEFINED',
150 1: 'PUBLISHED',
151 2: 'DRAFT',
152 3: 'AUTOMATED_ABUSE_TAKE_DOWN_HIDE',
153 4: 'AUTOMATED_ABUSE_TAKE_DOWN_DELETE',
154 13: 'AUTOMATED_ABUSE_REINSTATE',
155 10: 'AUTOMATED_OFF_TOPIC_HIDE',
156 14: 'AUTOMATED_FLAGGED_PENDING_MANUAL_REVIEW',
157 5: 'USER_FLAGGED_PENDING_MANUAL_REVIEW',
158 6: 'OWNER_DELETED',
159 7: 'MANUAL_TAKE_DOWN_HIDE',
160 17: 'MANUAL_PROFILE_TAKE_DOWN_SUSPEND',
161 8: 'MANUAL_TAKE_DOWN_DELETE',
162 18: 'REINSTATE_PROFILE_TAKEDOWN',
163 9: 'REINSTATE_ABUSE_TAKEDOWN',
164 11: 'CLEAR_OFF_TOPIC',
165 12: 'CONFIRM_OFF_TOPIC',
166 15: 'GOOGLER_OFF_TOPIC_HIDE',
167 16: 'EXPERT_FLAGGED_PENDING_MANUAL_REVIEW',
168};
169const kShadowBlockReason = {
170 0: 'REASON_UNDEFINED',
171 1: 'ULTRON_LOW_QUALITY',
172};
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100173
174export default class ExtraInfo {
175 constructor() {
176 this.lastProfile = {
177 body: {},
178 id: -1,
179 timestamp: 0,
180 };
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100181 this.lastCRsList = {
182 body: {},
183 id: -1,
184 duplicateNames: new Set(),
185 };
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100186 this.lastThread = {
187 body: {},
188 id: -1,
189 timestamp: 0,
190 };
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100191 this.lastThreadList = null;
192 this.lastThreadListTimestamp = 0;
193 this.lastThreadListRequestId = -1;
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100194 this.displayLanguage = getDisplayLanguage();
195 this.optionsWatcher = new OptionsWatcher(['extrainfo', 'perforumstats']);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100196 this.setUpHandlers();
197 }
198
199 setUpHandlers() {
200 window.addEventListener(kViewUnifiedUserResponseEvent, e => {
201 if (e.detail.id < this.lastProfile.id) return;
202
203 this.lastProfile = {
204 body: e.detail.body,
205 id: e.detail.id,
206 timestamp: Date.now(),
207 };
208 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100209 window.addEventListener(kListCannedResponsesResponse, e => {
210 if (e.detail.id < this.lastCRsList.id) return;
211
212 // Look if there are duplicate names
213 const crs = e.detail.body?.['1'] ?? [];
214 const names = crs.map(cr => cr?.['7']).slice().sort();
215 let duplicateNames = new Set();
216 for (let i = 1; i < names.length; i++)
217 if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
218
219 this.lastCRsList = {
220 body: e.detail.body,
221 id: e.detail.id,
222 duplicateNames,
223 };
224 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100225 window.addEventListener(kViewThreadResponse, e => {
226 if (e.detail.id < this.lastThread.id) return;
227
228 this.lastThread = {
229 body: e.detail.body,
230 id: e.detail.id,
231 timestamp: Date.now(),
232 };
233 });
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100234 window.addEventListener(kViewThreadResponse, e => {
235 if (e.detail.id < this.lastThread.id) return;
236
237 this.lastThread = {
238 body: e.detail.body,
239 id: e.detail.id,
240 timestamp: Date.now(),
241 };
242 });
243 window.addEventListener(kViewForumResponse, e => {
244 if (e.detail.id < this.lastThreadListRequestId) return;
245
246 this.lastThreadList = e.detail.body;
247 this.lastThreadListTimestamp = Date.now();
248 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100249 }
250
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100251 // Whether |feature| is enabled
252 isEnabled(feature) {
253 return this.optionsWatcher.isEnabled(feature);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100254 }
255
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100256 // Add chips which contain |info| to |node|. If |withContainer| is set to
257 // true, a container will contain all the chips.
258 addExtraInfoElement(info, node, withContainer = false) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100259 // Don't create if there's nothing to show
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100260 if (info.length == 0) return;
261
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100262 let container;
263 if (withContainer) {
264 container = document.createElement('div');
265 container.classList.add('TWPT-extrainfo-container');
266 } else {
267 container = node;
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100268 }
269
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100270 let tooltips = [];
271
272 for (const i of info) {
273 let chip = document.createElement('material-chip');
274 chip.classList.add('TWPT-extrainfo-chip');
275
276 let chipCont = document.createElement('div');
277 chipCont.classList.add('TWPT-chip-content-container');
278
279 let content = document.createElement('div');
280 content.classList.add('TWPT-content');
281
282 const [badge, badgeTooltip] = createExtBadge();
283
284 let span = document.createElement('span');
285 span.append(i);
286
287 content.append(badge, span);
288 chipCont.append(content);
289 chip.append(chipCont);
290 container.append(chip);
291
292 tooltips.push(badgeTooltip);
293 }
294
295 if (withContainer) node.append(container);
296
297 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100298 }
299
300 fieldInfo(field, value) {
301 let span = document.createElement('span');
302 span.append(document.createTextNode(field + ': '));
303
304 let valueEl = document.createElement('span');
305 valueEl.style.fontFamily = 'monospace';
306 valueEl.textContent = value;
307
308 span.append(valueEl);
309 return span;
310 }
311
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100312 /**
313 * Profile functionality
314 */
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100315 injectAtProfile(card) {
316 waitFor(
317 () => {
318 let now = Date.now();
319 if (now - this.lastProfile.timestamp < 15 * 1000)
320 return Promise.resolve(this.lastProfile);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100321 return Promise.reject(
322 new Error('Didn\'t receive profile information'));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100323 },
324 {
325 interval: 500,
326 timeout: 15 * 1000,
327 })
328 .then(profile => {
329 let info = [];
330 const abuseViolationCategory = profile.body?.['1']?.['6'];
331 if (abuseViolationCategory) {
332 info.push(this.fieldInfo(
333 'Abuse category',
334 kAbuseViolationCategories[abuseViolationCategory] ??
335 abuseViolationCategory));
336 }
337
338 const profileAbuse = profile.body?.['1']?.['1']?.['8'];
339
340 for (const [index, category] of kAbuseCategories) {
341 const violation = profileAbuse?.[index]?.['1']?.['1'];
342 if (violation) {
343 info.push(this.fieldInfo(
344 category + ' policy violation',
345 kAbuseViolationTypes[violation]));
346 }
347 }
348
349 const appealCount = profileAbuse?.['4'];
350 if (appealCount !== undefined)
351 info.push(this.fieldInfo('Number of appeals', appealCount));
352
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100353 this.addExtraInfoElement(info, card, true);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100354 })
355 .catch(err => {
356 console.error(
357 'extraInfo: error while injecting profile extra info: ', err);
358 });
359 }
360
361 injectAtProfileIfEnabled(card) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100362 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100363 if (isEnabled) return this.injectAtProfile(card);
364 });
365 }
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100366
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100367 /**
368 * Canned responses (CRs) functionality
369 */
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100370 getCRName(tags, isExpanded) {
371 if (!isExpanded)
372 return tags.parentNode?.querySelector?.('.text .name')?.textContent;
373
374 // https://www.youtube.com/watch?v=Z6_ZNW1DACE
375 return tags.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
376 ?.parentNode?.parentNode?.querySelector?.('.text .name')
377 ?.textContent;
378 }
379
380 // Inject usage stats in the |tags| component of a CR
381 injectAtCR(tags, isExpanded) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100382 waitFor(
383 () => {
384 if (this.lastCRsList.id != -1)
385 return Promise.resolve(this.lastCRsList);
386 return Promise.reject(
387 new Error('Didn\'t receive canned responses list'));
388 },
389 {
390 interval: 500,
391 timeout: 15 * 1000,
392 })
393 .then(crs => {
394 let name = this.getCRName(tags, isExpanded);
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100395
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100396 // If another CR has the same name, there's no easy way to distinguish
397 // them, so don't show the usage stats.
398 if (crs.duplicateNames.has(name)) {
399 console.info(
400 'CR "' + name +
401 '" is duplicate, so skipping the injection of usage stats.');
402 return;
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100403 }
404
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100405 for (const cr of (crs.body?.['1'] ?? [])) {
406 if (cr['7'] == name) {
407 let tag = document.createElement('material-chip');
408 tag.classList.add('TWPT-tag');
409
410 let container = document.createElement('div');
411 container.classList.add('TWPT-chip-content-container');
412
413 let content = document.createElement('div');
414 content.classList.add('TWPT-content');
415
416 const [badge, badgeTooltip] = createExtBadge();
417
418 let label = document.createElement('span');
419 label.textContent = 'Used ' + (cr['8'] ?? '0') + ' times';
420
421 content.append(badge, label);
422 container.append(content);
423 tag.append(container);
424 tags.append(tag);
425
426 new MDCTooltip(badgeTooltip);
427
428 if (cr['9']) {
429 const lastUsedTime = Math.floor(parseInt(cr['9']) / 1e3);
430 let date = (new Date(lastUsedTime)).toLocaleString();
431 createPlainTooltip(label, 'Last used: ' + date);
432 }
433
434 break;
435 }
436 }
437 })
438 .catch(err => {
439 console.error(
440 'extraInfo: error while injecting profile extra info: ', err);
441 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100442 }
443
444 injectAtCRIfEnabled(tags, isExpanded) {
445 // If the tag has already been injected, exit.
446 if (tags.querySelector('.TWPT-tag')) return;
447
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100448 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100449 if (isEnabled) return this.injectAtCR(tags, isExpanded);
450 });
451 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100452
453 /**
454 * Thread view functionality
455 */
456
457 getPendingStateInfo(endPendingStateTimestampMicros) {
458 const endPendingStateTimestamp =
459 Math.floor(endPendingStateTimestampMicros / 1e3);
460 const now = Date.now();
461 if (endPendingStateTimestampMicros && endPendingStateTimestamp > now) {
462 let span = document.createElement('span');
463 span.textContent = 'Only visible to badged users';
464
465 let date = new Date(endPendingStateTimestamp).toLocaleString();
466 let pendingTooltip =
467 createPlainTooltip(span, 'Visible after ' + date, false);
468 return [span, pendingTooltip];
469 }
470
471 return [null, null];
472 }
473
474 getMetadataInfo(itemMetadata) {
475 let info = [];
476
477 const state = itemMetadata?.['1'];
478 if (state && state != 1)
479 info.push(this.fieldInfo('State', kItemMetadataState[state] ?? state));
480
481 const shadowBlockInfo = itemMetadata?.['10'];
482 const blockedTimestampMicros = shadowBlockInfo?.['2'];
483 if (blockedTimestampMicros) {
484 const isBlocked = shadowBlockInfo?.['1'];
485 let span = document.createElement('span');
486 span.textContent =
487 isBlocked ? 'Shadow block active' : 'Shadow block no longer active';
488 if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
489 info.push(span);
490 }
491
492 return info;
493 }
494
495 getLiveReviewStatusInfo(liveReviewStatus) {
496 const verdict = liveReviewStatus?.['1'];
497 if (!verdict) return [null, null];
498 let label, labelClass;
499 switch (verdict) {
500 case 1: // LIVE_REVIEW_RELEVANT
501 label = 'Relevant';
502 labelClass = 'TWPT-extrainfo-good';
503 break;
504
505 case 2: // LIVE_REVIEW_OFF_TOPIC
506 label = 'Off-topic';
507 labelClass = 'TWPT-extrainfo-bad';
508 break;
509
510 case 3: // LIVE_REVIEW_ABUSE
511 label = 'Abuse';
512 labelClass = 'TWPT-extrainfo-bad';
513 break;
514 }
515 const reviewedBy = liveReviewStatus?.['2'];
516 const timestamp = liveReviewStatus?.['3'];
517 const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
518
519 let a = document.createElement('a');
520 a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
521 a.classList.add(labelClass);
522 a.textContent = 'Live review verdict: ' + label;
523 let liveReviewTooltip = createPlainTooltip(a, date, false);
524 return [a, liveReviewTooltip];
525 }
526
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100527 // Get an |info| array with the info related to the thread, and a |tooltips|
528 // array with the corresponding tooltips which should be initialized after the
529 // info is added to the DOM.
530 //
531 // This is used by the injectAtQuestion() and injectAtThreadList() functions.
532 getThreadInfo(thread) {
533 let info = [];
534 let tooltips = [];
535
536 const endPendingStateTimestampMicros = thread?.['2']?.['39'];
537 const [pendingStateInfo, pendingTooltip] =
538 this.getPendingStateInfo(endPendingStateTimestampMicros);
539 if (pendingStateInfo) info.push(pendingStateInfo);
540 if (pendingTooltip) tooltips.push(pendingTooltip);
541
542 const isTrending = thread?.['2']?.['25'];
543 const isTrendingAutoMarked = thread?.['39'];
544 if (isTrendingAutoMarked)
545 info.push(document.createTextNode('Automatically marked as trending'));
546 else if (isTrending)
547 info.push(document.createTextNode('Trending'));
548
549 const itemMetadata = thread?.['2']?.['12'];
550 const mdInfo = this.getMetadataInfo(itemMetadata);
551 info.push(...mdInfo);
552
553 const liveReviewStatus = thread?.['2']?.['38'];
554 const [liveReviewInfo, liveReviewTooltip] =
555 this.getLiveReviewStatusInfo(liveReviewStatus);
556 if (liveReviewInfo) info.push(liveReviewInfo);
557 if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
558
559 return [info, tooltips];
560 }
561
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100562 injectAtQuestion(stateChips) {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100563 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100564 if (currentPage === false) {
565 console.error('extraInfo: couldn\'t parse current URL:', location.href);
566 return;
567 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100568
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100569 waitFor(
570 () => {
571 let now = Date.now();
572 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
573 if (now - this.lastThread.timestamp < 30 * 1000 &&
574 threadInfo?.['1'] == currentPage.thread &&
575 threadInfo?.['3'] == currentPage.forum)
576 return Promise.resolve(this.lastThread);
577 return Promise.reject(
578 new Error('Didn\'t receive thread information'));
579 },
580 {
581 interval: 500,
582 timeout: 30 * 1000,
583 })
584 .then(thread => {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100585 const [info, tooltips] = this.getThreadInfo(thread.body?.['1']);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100586 this.addExtraInfoElement(info, stateChips, false);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100587 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100588 })
589 .catch(err => {
590 console.error(
591 'extraInfo: error while injecting question extra info: ', err);
592 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100593 }
594
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100595 injectAtQuestionIfEnabled(stateChips) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100596 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100597 if (isEnabled) return this.injectAtQuestion(stateChips);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100598 });
599 }
600
601 getMessagesByType(thread, type) {
602 if (type === 'reply') return thread?.['1']?.['3'];
603 if (type === 'lastMessage') return thread?.['1']?.['17']?.['3'];
604 if (type === 'suggested') return thread?.['1']?.['17']?.['4'];
605 if (type === 'recommended') return thread?.['1']?.['17']?.['1'];
606 }
607
608 getMessageByTypeAndIndex(thread, type, index) {
609 return this.getMessagesByType(thread, type)?.[index];
610 }
611
612 // Returns true if the last message is included in the messages array (in the
613 // extension context, we say those messages are of the type "reply").
614 lastMessageInReplies(thread) {
615 const lastMessageId = thread?.['1']?.['17']?.['3']?.[0]?.['1']?.['1'];
616 if (!lastMessageId) return true;
617
618 // If the last message is included in the lastMessage array, check if it
619 // also exists in the messages/replies array.
620 const replies = thread?.['1']?.['3'];
621 if (!replies?.length) return false;
622 const lastReplyIndex = replies.length - 1;
623 const lastReplyId = replies[lastReplyIndex]?.['1']?.['1'];
624 return lastMessageId && lastMessageId == lastReplyId;
625 }
626
627 getMessageInfo(thread, message) {
628 const section = message.parentNode;
629
630 let type = 'reply';
631 if (section?.querySelector?.('.heading material-icon[icon="auto_awesome"]'))
632 type = 'suggested';
633 if (section?.querySelector?.('.heading material-icon[icon="check_circle"]'))
634 type = 'recommended';
635
636 let index = -1;
637 let messagesInDom = section.querySelectorAll('ec-message');
638
639 // Number of messages in the DOM.
640 const n = messagesInDom.length;
641
642 if (type !== 'reply') {
643 for (let i = 0; i < n; ++i) {
644 if (message.isEqualNode(messagesInDom[i])) {
645 index = i;
646 break;
647 }
648 }
649 } else {
650 // If the type of the message is a reply, things are slightly more
651 // complex, since replies are paginated and the last message should be
652 // treated separately (it is included diferently in the API response).
653 let lastMessageInReplies = this.lastMessageInReplies(thread);
654 if (message.isEqualNode(messagesInDom[n - 1]) && !lastMessageInReplies) {
655 type = 'lastMessage';
656 index = 0
657 } else {
658 // Number of messages in the current API response.
659 const messagesInResponse = this.getMessagesByType(thread, type);
660 const m = messagesInResponse.length;
661 // If the last message is included in the replies array, we also have to
662 // consider the last message in the DOM.
663 let modifier = lastMessageInReplies ? 1 : 0;
664 for (let k = 0; k < m; ++k) {
665 let i = n - 2 - k + modifier;
666 if (message.isEqualNode(messagesInDom[i])) {
667 index = m - 1 - k;
668 break;
669 }
670 }
671 }
672 }
673
674 return [type, index];
675 }
676
677 injectAtMessage(messageNode) {
678 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100679 if (currentPage === false) {
680 console.error('extraInfo: couldn\'t parse current URL:', location.href);
681 return;
682 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100683
684 let footer = messageNode.querySelector('.footer-fill');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100685 if (!footer) {
686 console.error('extraInfo: message doesn\'t have a footer:', messageNode);
687 return;
688 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100689
690 const [type, index] =
691 this.getMessageInfo(this.lastThread.body, messageNode);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100692 if (index == -1) {
693 console.error('extraInfo: this.getMessageInfo() returned index -1.');
694 return;
695 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100696
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100697 waitFor(
698 () => {
699 let now = Date.now();
700 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
701 if (now - this.lastThread.timestamp < 30 * 1000 &&
702 threadInfo?.['1'] == currentPage.thread &&
703 threadInfo?.['3'] == currentPage.forum) {
704 const message = this.getMessageByTypeAndIndex(
705 this.lastThread.body, type, index);
706 if (message) return Promise.resolve(message);
707 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100708
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100709 return Promise.reject(new Error(
710 'Didn\'t receive thread information (type: ' + type +
711 ', index: ' + index + ')'));
712 },
713 {
714 interval: 1000,
715 timeout: 30 * 1000,
716 })
717 .then(message => {
718 let info = [];
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100719
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100720 const endPendingStateTimestampMicros = message['1']?.['17'];
721 const [pendingStateInfo, pendingTooltip] =
722 this.getPendingStateInfo(endPendingStateTimestampMicros);
723 if (pendingStateInfo) info.push(pendingStateInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100724
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100725 const itemMetadata = message['1']?.['5'];
726 const mdInfo = this.getMetadataInfo(itemMetadata);
727 info.push(...mdInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100728
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100729 const liveReviewStatus = message['1']?.['36'];
730 const [liveReviewInfo, liveReviewTooltip] =
731 this.getLiveReviewStatusInfo(liveReviewStatus);
732 if (liveReviewInfo) info.push(liveReviewInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100733
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100734 this.addExtraInfoElement(info, footer, true);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100735 if (pendingTooltip) new MDCTooltip(pendingTooltip);
736 if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
737 })
738 .catch(err => {
739 console.error(
740 'extraInfo: error while injecting message extra info: ', err);
741 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100742 }
743
744 injectAtMessageIfEnabled(message) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100745 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100746 if (isEnabled) return this.injectAtMessage(message);
747 });
748 }
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100749
750 /**
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100751 * Thread list functionality
752 */
753 injectAtThreadList(li) {
754 waitFor(
755 () => {
756 const header = li.querySelector(
757 'ec-thread-summary .main-header .panel-description a.header');
758 if (header === null) {
759 console.error(
760 'extraInfo: Header is not present in the thread item\'s DOM.');
761 return;
762 }
763
764 const threadInfo = parseUrl(header.href);
765 if (threadInfo === false) {
766 console.error('extraInfo: Thread\'s link cannot be parsed.');
767 return;
768 }
769
770 let authorLine = li.querySelector(
771 'ec-thread-summary .header-content .top-row .author-line');
772 if (!authorLine) {
773 console.error(
774 'extraInfo: Author line is not present in the thread item\'s DOM.');
775 return;
776 }
777
778 let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
779 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
780 t?.['2']?.['1']?.['3'] == threadInfo.forum;
781 });
782 if (thread) return Promise.resolve([thread, authorLine]);
783 return Promise.reject(
784 new Error('Didn\'t receive thread information'));
785 },
786 {
787 interval: 500,
788 timeout: 7 * 1000,
789 })
790 .then(response => {
791 const [thread, authorLine] = response;
792 const state = thread?.['2']?.['12']?.['1'];
793 if (state && ![1, 13, 18, 9].includes(state)) {
794 let label = document.createElement('div');
795 label.classList.add('TWPT-label');
796
797 const [badge, badgeTooltip] = createExtBadge();
798
799 let span = document.createElement('span');
800 span.textContent = kItemMetadataState[state] ?? 'State ' + state;
801
802 label.append(badge, span);
803 authorLine.prepend(label);
804 new MDCTooltip(badgeTooltip);
805 }
806 })
807 .catch(err => {
808 console.error(
809 'extraInfo: error while injecting thread list extra info: ', err);
810 });
811 }
812
813 injectAtThreadListIfEnabled(li) {
814 this.isEnabled('extrainfo').then(isEnabled => {
815 if (isEnabled) this.injectAtThreadList(li);
816 });
817 }
818
819 injectAtExpandedThreadList(toolbelt) {
820 const header =
821 toolbelt?.parentNode?.parentNode?.parentNode?.querySelector?.(
822 '.main-header .panel-description a.header');
823 if (header === null) {
824 console.error(
825 'extraInfo: Header is not present in the thread item\'s DOM.');
826 return;
827 }
828
829 const threadInfo = parseUrl(header.href);
830 if (threadInfo === false) {
831 console.error('extraInfo: Thread\'s link cannot be parsed.');
832 return;
833 }
834
835 waitFor(
836 () => {
837 let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
838 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
839 t?.['2']?.['1']?.['3'] == threadInfo.forum;
840 });
841 if (thread) return Promise.resolve(thread);
842 return Promise.reject(
843 new Error('Didn\'t receive thread information'));
844 },
845 {
846 interval: 500,
847 timeout: 7 * 1000,
848 })
849 .then(thread => {
850 const [info, tooltips] = this.getThreadInfo(thread);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100851 this.addExtraInfoElement(info, toolbelt, true);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100852 for (const tooltip of tooltips) new MDCTooltip(tooltip);
853 })
854 .catch(err => {
855 console.error(
856 'extraInfo: error while injecting thread list extra info: ', err);
857 });
858 }
859
860 injectAtExpandedThreadListIfEnabled(toolbelt) {
861 this.isEnabled('extrainfo').then(isEnabled => {
862 if (isEnabled) this.injectAtExpandedThreadList(toolbelt);
863 });
864 }
865
866 /**
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100867 * Per-forum stats in user profiles.
868 */
869
870 injectPerForumStats(chart) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100871 waitFor(
872 () => {
873 let now = Date.now();
874 if (now - this.lastProfile.timestamp < 15 * 1000)
875 return Promise.resolve(this.lastProfile);
876 return Promise.reject(new Error(
877 'Didn\'t receive profile information (for per-profile stats)'));
878 },
879 {
880 interval: 500,
881 timeout: 15 * 1000,
882 })
883 .then(profile => {
Adrià Vilanova Martínez69c30502022-01-28 20:47:08 +0100884 new PerForumStatsSection(
885 chart?.parentNode, profile.body, this.displayLanguage);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100886 })
887 .catch(err => {
888 console.error(
889 'extraInfo: error while preparing to inject per-forum stats: ',
890 err);
891 });
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100892 }
893
894 injectPerForumStatsIfEnabled(chart) {
895 this.isEnabled('perforumstats').then(isEnabled => {
896 if (isEnabled) this.injectPerForumStats(chart);
897 });
898 }
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100899}