blob: b78b2d0559174a24ce5a0f00e9998fa7acb9c2e6 [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ínez7e8796c2022-01-23 21:46:46 +01009
10const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +010011const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +010012const kViewThreadResponse = 'TWPT_ViewThreadResponse';
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010013
14const kAbuseCategories = [
15 ['1', 'Account'],
16 ['2', 'Display name'],
17 ['3', 'Avatar'],
18];
19const kAbuseViolationCategories = {
20 0: 'NO_VIOLATION',
21 1: 'COMMUNITY_POLICY_VIOLATION',
22 2: 'LEGAL_VIOLATION',
23 3: 'CSAI_VIOLATION',
24 4: 'OTHER_VIOLATION',
25};
26const kAbuseViolationTypes = {
27 0: 'UNSPECIFIED',
28 23: 'ACCOUNT_DISABLED',
29 55: 'ACCOUNT_HAS_SERVICES_DISABLED',
30 35: 'ACCOUNT_HIJACKED',
31 96: 'ACCOUNT_LEAKED_CREDENTIALS',
32 92: 'ACCOUNT_NOT_SUPPORTED',
33 81: 'ARTISTIC_NUDITY',
34 66: 'BAD_BEHAVIOR_PATTERN',
35 78: 'BAD_ENGAGEMENT_BEHAVIOR_PATTERN',
36 79: 'BORDERLINE_HARASSMENT',
37 80: 'BORDERLINE_HATE_SPEECH',
38 38: 'BOTNET',
39 32: 'BRANDING_VIOLATION',
40 100: 'CAPITALIZING_TRAGIC_EVENTS',
41 105: 'CLOAKING',
42 49: 'COIN_MINING',
43 7: 'COMMERCIAL_CONTENT',
44 97: 'COPPA_REGULATED',
45 57: 'COPYRIGHT_CIRCUMVENTION',
46 8: 'COPYRIGHTED_CONTENT',
47 58: 'COURT_ORDER',
48 51: 'CSAI',
49 94: 'CSAI_INSPECT',
50 52: 'CSAI_CARTOON_HUMOR',
51 53: 'CSAI_SOLICITATION',
52 108: 'CSAI_NON_APPARENT',
53 67: 'DANGEROUS',
54 37: 'DATA_SCRAPING',
55 86: 'DECEPTIVE_OAUTH_IMPLEMENTATION',
56 46: 'DEFAMATORY_CONTENT',
57 36: 'DELINQUENT_BILLING',
58 30: 'DISRUPTION_ATTEMPT',
59 112: 'DOMESTIC_INTERFERENCE',
60 22: 'DOS',
61 9: 'DUPLICATE_CONTENT',
62 68: 'DUPLICATE_LOCAL_PAGE',
63 121: 'NON_QUALIFYING_ORGANIZATION',
64 115: 'EGREGIOUS_INTERACTION_WITH_MINOR',
65 83: 'ENGAGEMENT_COLLUSION',
66 41: 'EXPLOIT_ATTACKS',
67 65: 'FAKE_USER',
68 2: 'FRAUD',
69 21: 'FREE_TRIAL_VIOLATION',
70 43: 'GIBBERISH',
71 101: 'FOREIGN_INTERFERENCE',
72 59: 'GOVERNMENT_ORDER',
73 10: 'GRAPHICAL_VIOLENCE',
74 11: 'HARASSMENT',
75 12: 'HATE_SPEECH',
76 90: 'IDENTICAL_PRODUCT_NAME',
77 60: 'ILLEGAL_DRUGS',
78 13: 'IMPERSONATION',
79 69: 'IMPERSONATION_WITH_PII',
80 116: 'INAPPROPRIATE_INTERACTION_WITH_MINOR',
81 45: 'INAPPROPRIATE_CONTENT_SPEECH',
82 106: 'INTENTIONAL_THWARTING',
83 27: 'INTRUSION_ATTEMPT',
84 87: 'INVALID_API_USAGE',
85 14: 'INVALID_CONTENT',
86 20: 'INVALID_GCE_USAGE',
87 120: 'INVALID_STORAGE_USAGE',
88 15: 'INVALID_IMAGE_QUALITY',
89 88: 'INVALID_API_PRIVACY_POLICY_DISCLOSURE',
90 54: 'INVALID_USAGE_OF_IP_PROXYING',
91 99: 'KEYWORD_STUFFING',
92 61: 'LEGAL_COUNTERFEIT',
93 62: 'LEGAL_EXPORT',
94 63: 'LEGAL_PRIVACY',
95 33: 'LEGAL_REVIEW',
96 91: 'LEGAL_PROTECTED',
97 70: 'LOW_QUALITY_CONTENT',
98 93: 'LOW_REPUTATION_PHONE_NUMBER',
99 6: 'MALICIOUS_SOFTWARE',
100 40: 'MALWARE',
101 113: 'MISLEADING',
102 114: 'MISREP_OF_ID',
103 89: 'MEMBER_OF_ABUSIVE_GCE_NETWORK',
104 84: 'NON_CONSENSUAL_EXPLICIT_IMAGERY',
105 1: 'NONE',
106 102: 'OFF_TOPIC',
107 31: 'OPEN_PROXY',
108 28: 'PAYMENT_FRAUD',
109 16: 'PEDOPHILIA',
110 71: 'PERSONAL_INFORMATION_CONTENT',
111 25: 'PHISHING',
112 34: 'POLICY_REVIEW',
113 17: 'PORNOGRAPHY',
114 29: 'QUOTA_CIRCUMVENTION',
115 72: 'QUOTA_EXCEEDED',
116 73: 'REGULATED',
117 24: 'REPEATED_POLICY_VIOLATION',
118 104: 'RESOURCE_COMPROMISED',
119 107: 'REWARD_PROGRAMS_ABUSE',
120 74: 'ROGUE_PHARMA',
121 82: 'ESCORT',
122 75: 'SPAMMY_LOCAL_VERTICAL',
123 39: 'SEND_EMAIL_SPAM',
124 117: 'SEXTORTION',
125 118: 'SEX_TRAFFICKING',
126 44: 'SEXUALLY_EXPLICIT_CONTENT',
127 3: 'SHARDING',
128 95: 'SOCIAL_ENGINEERING',
129 109: 'SUSPICIOUS',
130 19: 'TRADEMARK_CONTENT',
131 50: 'TRAFFIC_PUMPING',
132 76: 'UNSAFE_RACY',
133 103: 'UNUSUAL_ACTIVITY_ALERT',
134 64: 'UNWANTED_CONTENT',
135 26: 'UNWANTED_SOFTWARE',
136 77: 'VIOLENT_EXTREMISM',
137 119: 'UNAUTH_IMAGES_OF_MINORS',
138 85: 'UNAUTHORIZED_SERVICE_RESELLING',
139 98: 'CSAI_EXTERNAL',
140 5: 'SPAM',
141 4: 'UNSAFE',
142 47: 'CHILD_PORNOGRAPHY_INCITATION',
143 18: 'TERRORISM_SUPPORT',
144 56: 'CSAI_WORST_OF_WORST',
145};
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100146const kItemMetadataState = {
147 0: 'UNDEFINED',
148 1: 'PUBLISHED',
149 2: 'DRAFT',
150 3: 'AUTOMATED_ABUSE_TAKE_DOWN_HIDE',
151 4: 'AUTOMATED_ABUSE_TAKE_DOWN_DELETE',
152 13: 'AUTOMATED_ABUSE_REINSTATE',
153 10: 'AUTOMATED_OFF_TOPIC_HIDE',
154 14: 'AUTOMATED_FLAGGED_PENDING_MANUAL_REVIEW',
155 5: 'USER_FLAGGED_PENDING_MANUAL_REVIEW',
156 6: 'OWNER_DELETED',
157 7: 'MANUAL_TAKE_DOWN_HIDE',
158 17: 'MANUAL_PROFILE_TAKE_DOWN_SUSPEND',
159 8: 'MANUAL_TAKE_DOWN_DELETE',
160 18: 'REINSTATE_PROFILE_TAKEDOWN',
161 9: 'REINSTATE_ABUSE_TAKEDOWN',
162 11: 'CLEAR_OFF_TOPIC',
163 12: 'CONFIRM_OFF_TOPIC',
164 15: 'GOOGLER_OFF_TOPIC_HIDE',
165 16: 'EXPERT_FLAGGED_PENDING_MANUAL_REVIEW',
166};
167const kShadowBlockReason = {
168 0: 'REASON_UNDEFINED',
169 1: 'ULTRON_LOW_QUALITY',
170};
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100171
172export default class ExtraInfo {
173 constructor() {
174 this.lastProfile = {
175 body: {},
176 id: -1,
177 timestamp: 0,
178 };
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100179 this.lastCRsList = {
180 body: {},
181 id: -1,
182 duplicateNames: new Set(),
183 };
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100184 this.lastThread = {
185 body: {},
186 id: -1,
187 timestamp: 0,
188 };
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100189 this.displayLanguage = getDisplayLanguage();
190 this.optionsWatcher = new OptionsWatcher(['extrainfo', 'perforumstats']);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100191 this.setUpHandlers();
192 }
193
194 setUpHandlers() {
195 window.addEventListener(kViewUnifiedUserResponseEvent, e => {
196 if (e.detail.id < this.lastProfile.id) return;
197
198 this.lastProfile = {
199 body: e.detail.body,
200 id: e.detail.id,
201 timestamp: Date.now(),
202 };
203 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100204 window.addEventListener(kListCannedResponsesResponse, e => {
205 if (e.detail.id < this.lastCRsList.id) return;
206
207 // Look if there are duplicate names
208 const crs = e.detail.body?.['1'] ?? [];
209 const names = crs.map(cr => cr?.['7']).slice().sort();
210 let duplicateNames = new Set();
211 for (let i = 1; i < names.length; i++)
212 if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
213
214 this.lastCRsList = {
215 body: e.detail.body,
216 id: e.detail.id,
217 duplicateNames,
218 };
219 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100220 window.addEventListener(kViewThreadResponse, e => {
221 if (e.detail.id < this.lastThread.id) return;
222
223 this.lastThread = {
224 body: e.detail.body,
225 id: e.detail.id,
226 timestamp: Date.now(),
227 };
228 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100229 }
230
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100231 // Whether |feature| is enabled
232 isEnabled(feature) {
233 return this.optionsWatcher.isEnabled(feature);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100234 }
235
236 // Add a pretty component which contains |info| to |node|.
237 addExtraInfoElement(info, node) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100238 // Don't create if there's nothing to show
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100239 if (info.length == 0) return;
240
241 let container = document.createElement('div');
242 container.classList.add('TWPT-extrainfo-container');
243
244 let badgeCell = document.createElement('div');
245 badgeCell.classList.add('TWPT-extrainfo-badge-cell');
246
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100247 const [badge, badgeTooltip] = createExtBadge();
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100248 badgeCell.append(badge);
249
250 let infoCell = document.createElement('div');
251 infoCell.classList.add('TWPT-extrainfo-info-cell');
252
253 for (const i of info) {
254 let iRow = document.createElement('div');
255 iRow.append(i);
256 infoCell.append(iRow);
257 }
258
259 container.append(badgeCell, infoCell);
260 node.append(container);
261 new MDCTooltip(badgeTooltip);
262 }
263
264 fieldInfo(field, value) {
265 let span = document.createElement('span');
266 span.append(document.createTextNode(field + ': '));
267
268 let valueEl = document.createElement('span');
269 valueEl.style.fontFamily = 'monospace';
270 valueEl.textContent = value;
271
272 span.append(valueEl);
273 return span;
274 }
275
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100276 /**
277 * Profile functionality
278 */
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100279 injectAtProfile(card) {
280 waitFor(
281 () => {
282 let now = Date.now();
283 if (now - this.lastProfile.timestamp < 15 * 1000)
284 return Promise.resolve(this.lastProfile);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100285 return Promise.reject(
286 new Error('Didn\'t receive profile information'));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100287 },
288 {
289 interval: 500,
290 timeout: 15 * 1000,
291 })
292 .then(profile => {
293 let info = [];
294 const abuseViolationCategory = profile.body?.['1']?.['6'];
295 if (abuseViolationCategory) {
296 info.push(this.fieldInfo(
297 'Abuse category',
298 kAbuseViolationCategories[abuseViolationCategory] ??
299 abuseViolationCategory));
300 }
301
302 const profileAbuse = profile.body?.['1']?.['1']?.['8'];
303
304 for (const [index, category] of kAbuseCategories) {
305 const violation = profileAbuse?.[index]?.['1']?.['1'];
306 if (violation) {
307 info.push(this.fieldInfo(
308 category + ' policy violation',
309 kAbuseViolationTypes[violation]));
310 }
311 }
312
313 const appealCount = profileAbuse?.['4'];
314 if (appealCount !== undefined)
315 info.push(this.fieldInfo('Number of appeals', appealCount));
316
317 this.addExtraInfoElement(info, card);
318 })
319 .catch(err => {
320 console.error(
321 'extraInfo: error while injecting profile extra info: ', err);
322 });
323 }
324
325 injectAtProfileIfEnabled(card) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100326 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100327 if (isEnabled) return this.injectAtProfile(card);
328 });
329 }
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100330
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100331 /**
332 * Canned responses (CRs) functionality
333 */
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100334 getCRName(tags, isExpanded) {
335 if (!isExpanded)
336 return tags.parentNode?.querySelector?.('.text .name')?.textContent;
337
338 // https://www.youtube.com/watch?v=Z6_ZNW1DACE
339 return tags.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
340 ?.parentNode?.parentNode?.querySelector?.('.text .name')
341 ?.textContent;
342 }
343
344 // Inject usage stats in the |tags| component of a CR
345 injectAtCR(tags, isExpanded) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100346 waitFor(
347 () => {
348 if (this.lastCRsList.id != -1)
349 return Promise.resolve(this.lastCRsList);
350 return Promise.reject(
351 new Error('Didn\'t receive canned responses list'));
352 },
353 {
354 interval: 500,
355 timeout: 15 * 1000,
356 })
357 .then(crs => {
358 let name = this.getCRName(tags, isExpanded);
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100359
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100360 // If another CR has the same name, there's no easy way to distinguish
361 // them, so don't show the usage stats.
362 if (crs.duplicateNames.has(name)) {
363 console.info(
364 'CR "' + name +
365 '" is duplicate, so skipping the injection of usage stats.');
366 return;
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100367 }
368
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100369 for (const cr of (crs.body?.['1'] ?? [])) {
370 if (cr['7'] == name) {
371 let tag = document.createElement('material-chip');
372 tag.classList.add('TWPT-tag');
373
374 let container = document.createElement('div');
375 container.classList.add('TWPT-chip-content-container');
376
377 let content = document.createElement('div');
378 content.classList.add('TWPT-content');
379
380 const [badge, badgeTooltip] = createExtBadge();
381
382 let label = document.createElement('span');
383 label.textContent = 'Used ' + (cr['8'] ?? '0') + ' times';
384
385 content.append(badge, label);
386 container.append(content);
387 tag.append(container);
388 tags.append(tag);
389
390 new MDCTooltip(badgeTooltip);
391
392 if (cr['9']) {
393 const lastUsedTime = Math.floor(parseInt(cr['9']) / 1e3);
394 let date = (new Date(lastUsedTime)).toLocaleString();
395 createPlainTooltip(label, 'Last used: ' + date);
396 }
397
398 break;
399 }
400 }
401 })
402 .catch(err => {
403 console.error(
404 'extraInfo: error while injecting profile extra info: ', err);
405 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100406 }
407
408 injectAtCRIfEnabled(tags, isExpanded) {
409 // If the tag has already been injected, exit.
410 if (tags.querySelector('.TWPT-tag')) return;
411
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100412 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100413 if (isEnabled) return this.injectAtCR(tags, isExpanded);
414 });
415 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100416
417 /**
418 * Thread view functionality
419 */
420
421 getPendingStateInfo(endPendingStateTimestampMicros) {
422 const endPendingStateTimestamp =
423 Math.floor(endPendingStateTimestampMicros / 1e3);
424 const now = Date.now();
425 if (endPendingStateTimestampMicros && endPendingStateTimestamp > now) {
426 let span = document.createElement('span');
427 span.textContent = 'Only visible to badged users';
428
429 let date = new Date(endPendingStateTimestamp).toLocaleString();
430 let pendingTooltip =
431 createPlainTooltip(span, 'Visible after ' + date, false);
432 return [span, pendingTooltip];
433 }
434
435 return [null, null];
436 }
437
438 getMetadataInfo(itemMetadata) {
439 let info = [];
440
441 const state = itemMetadata?.['1'];
442 if (state && state != 1)
443 info.push(this.fieldInfo('State', kItemMetadataState[state] ?? state));
444
445 const shadowBlockInfo = itemMetadata?.['10'];
446 const blockedTimestampMicros = shadowBlockInfo?.['2'];
447 if (blockedTimestampMicros) {
448 const isBlocked = shadowBlockInfo?.['1'];
449 let span = document.createElement('span');
450 span.textContent =
451 isBlocked ? 'Shadow block active' : 'Shadow block no longer active';
452 if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
453 info.push(span);
454 }
455
456 return info;
457 }
458
459 getLiveReviewStatusInfo(liveReviewStatus) {
460 const verdict = liveReviewStatus?.['1'];
461 if (!verdict) return [null, null];
462 let label, labelClass;
463 switch (verdict) {
464 case 1: // LIVE_REVIEW_RELEVANT
465 label = 'Relevant';
466 labelClass = 'TWPT-extrainfo-good';
467 break;
468
469 case 2: // LIVE_REVIEW_OFF_TOPIC
470 label = 'Off-topic';
471 labelClass = 'TWPT-extrainfo-bad';
472 break;
473
474 case 3: // LIVE_REVIEW_ABUSE
475 label = 'Abuse';
476 labelClass = 'TWPT-extrainfo-bad';
477 break;
478 }
479 const reviewedBy = liveReviewStatus?.['2'];
480 const timestamp = liveReviewStatus?.['3'];
481 const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
482
483 let a = document.createElement('a');
484 a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
485 a.classList.add(labelClass);
486 a.textContent = 'Live review verdict: ' + label;
487 let liveReviewTooltip = createPlainTooltip(a, date, false);
488 return [a, liveReviewTooltip];
489 }
490
491 injectAtQuestion(question) {
492 let currentPage = parseUrl(location.href);
493 if (currentPage === false) return;
494
495 let content = question.querySelector('ec-question > .content');
496 if (!content) return;
497
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100498 waitFor(
499 () => {
500 let now = Date.now();
501 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
502 if (now - this.lastThread.timestamp < 30 * 1000 &&
503 threadInfo?.['1'] == currentPage.thread &&
504 threadInfo?.['3'] == currentPage.forum)
505 return Promise.resolve(this.lastThread);
506 return Promise.reject(
507 new Error('Didn\'t receive thread information'));
508 },
509 {
510 interval: 500,
511 timeout: 30 * 1000,
512 })
513 .then(thread => {
514 let info = [];
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100515
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100516 const endPendingStateTimestampMicros =
517 thread.body['1']?.['2']?.['39'];
518 const [pendingStateInfo, pendingTooltip] =
519 this.getPendingStateInfo(endPendingStateTimestampMicros);
520 if (pendingStateInfo) info.push(pendingStateInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100521
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100522 const isTrending = thread.body['1']?.['2']?.['25'];
523 const isTrendingAutoMarked = thread.body['1']?.['39'];
524 if (isTrendingAutoMarked)
525 info.push(
526 document.createTextNode('Automatically marked as trending'));
527 else if (isTrending)
528 info.push(document.createTextNode('Trending'));
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100529
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100530 const itemMetadata = thread.body['1']?.['2']?.['12'];
531 const mdInfo = this.getMetadataInfo(itemMetadata);
532 info.push(...mdInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100533
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100534 const liveReviewStatus = thread.body['1']?.['2']?.['38'];
535 const [liveReviewInfo, liveReviewTooltip] =
536 this.getLiveReviewStatusInfo(liveReviewStatus);
537 if (liveReviewInfo) info.push(liveReviewInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100538
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100539 this.addExtraInfoElement(info, content);
540 if (pendingTooltip) new MDCTooltip(pendingTooltip);
541 if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
542 })
543 .catch(err => {
544 console.error(
545 'extraInfo: error while injecting question extra info: ', err);
546 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100547 }
548
549 injectAtQuestionIfEnabled(question) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100550 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100551 if (isEnabled) return this.injectAtQuestion(question);
552 });
553 }
554
555 getMessagesByType(thread, type) {
556 if (type === 'reply') return thread?.['1']?.['3'];
557 if (type === 'lastMessage') return thread?.['1']?.['17']?.['3'];
558 if (type === 'suggested') return thread?.['1']?.['17']?.['4'];
559 if (type === 'recommended') return thread?.['1']?.['17']?.['1'];
560 }
561
562 getMessageByTypeAndIndex(thread, type, index) {
563 return this.getMessagesByType(thread, type)?.[index];
564 }
565
566 // Returns true if the last message is included in the messages array (in the
567 // extension context, we say those messages are of the type "reply").
568 lastMessageInReplies(thread) {
569 const lastMessageId = thread?.['1']?.['17']?.['3']?.[0]?.['1']?.['1'];
570 if (!lastMessageId) return true;
571
572 // If the last message is included in the lastMessage array, check if it
573 // also exists in the messages/replies array.
574 const replies = thread?.['1']?.['3'];
575 if (!replies?.length) return false;
576 const lastReplyIndex = replies.length - 1;
577 const lastReplyId = replies[lastReplyIndex]?.['1']?.['1'];
578 return lastMessageId && lastMessageId == lastReplyId;
579 }
580
581 getMessageInfo(thread, message) {
582 const section = message.parentNode;
583
584 let type = 'reply';
585 if (section?.querySelector?.('.heading material-icon[icon="auto_awesome"]'))
586 type = 'suggested';
587 if (section?.querySelector?.('.heading material-icon[icon="check_circle"]'))
588 type = 'recommended';
589
590 let index = -1;
591 let messagesInDom = section.querySelectorAll('ec-message');
592
593 // Number of messages in the DOM.
594 const n = messagesInDom.length;
595
596 if (type !== 'reply') {
597 for (let i = 0; i < n; ++i) {
598 if (message.isEqualNode(messagesInDom[i])) {
599 index = i;
600 break;
601 }
602 }
603 } else {
604 // If the type of the message is a reply, things are slightly more
605 // complex, since replies are paginated and the last message should be
606 // treated separately (it is included diferently in the API response).
607 let lastMessageInReplies = this.lastMessageInReplies(thread);
608 if (message.isEqualNode(messagesInDom[n - 1]) && !lastMessageInReplies) {
609 type = 'lastMessage';
610 index = 0
611 } else {
612 // Number of messages in the current API response.
613 const messagesInResponse = this.getMessagesByType(thread, type);
614 const m = messagesInResponse.length;
615 // If the last message is included in the replies array, we also have to
616 // consider the last message in the DOM.
617 let modifier = lastMessageInReplies ? 1 : 0;
618 for (let k = 0; k < m; ++k) {
619 let i = n - 2 - k + modifier;
620 if (message.isEqualNode(messagesInDom[i])) {
621 index = m - 1 - k;
622 break;
623 }
624 }
625 }
626 }
627
628 return [type, index];
629 }
630
631 injectAtMessage(messageNode) {
632 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100633 if (currentPage === false) {
634 console.error('extraInfo: couldn\'t parse current URL:', location.href);
635 return;
636 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100637
638 let footer = messageNode.querySelector('.footer-fill');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100639 if (!footer) {
640 console.error('extraInfo: message doesn\'t have a footer:', messageNode);
641 return;
642 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100643
644 const [type, index] =
645 this.getMessageInfo(this.lastThread.body, messageNode);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100646 if (index == -1) {
647 console.error('extraInfo: this.getMessageInfo() returned index -1.');
648 return;
649 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100650
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100651 waitFor(
652 () => {
653 let now = Date.now();
654 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
655 if (now - this.lastThread.timestamp < 30 * 1000 &&
656 threadInfo?.['1'] == currentPage.thread &&
657 threadInfo?.['3'] == currentPage.forum) {
658 const message = this.getMessageByTypeAndIndex(
659 this.lastThread.body, type, index);
660 if (message) return Promise.resolve(message);
661 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100662
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100663 return Promise.reject(new Error(
664 'Didn\'t receive thread information (type: ' + type +
665 ', index: ' + index + ')'));
666 },
667 {
668 interval: 1000,
669 timeout: 30 * 1000,
670 })
671 .then(message => {
672 let info = [];
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100673
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100674 const endPendingStateTimestampMicros = message['1']?.['17'];
675 const [pendingStateInfo, pendingTooltip] =
676 this.getPendingStateInfo(endPendingStateTimestampMicros);
677 if (pendingStateInfo) info.push(pendingStateInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100678
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100679 const itemMetadata = message['1']?.['5'];
680 const mdInfo = this.getMetadataInfo(itemMetadata);
681 info.push(...mdInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100682
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100683 const liveReviewStatus = message['1']?.['36'];
684 const [liveReviewInfo, liveReviewTooltip] =
685 this.getLiveReviewStatusInfo(liveReviewStatus);
686 if (liveReviewInfo) info.push(liveReviewInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100687
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100688 this.addExtraInfoElement(info, footer);
689 if (pendingTooltip) new MDCTooltip(pendingTooltip);
690 if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
691 })
692 .catch(err => {
693 console.error(
694 'extraInfo: error while injecting message extra info: ', err);
695 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100696 }
697
698 injectAtMessageIfEnabled(message) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100699 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100700 if (isEnabled) return this.injectAtMessage(message);
701 });
702 }
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100703
704 /**
705 * Per-forum stats in user profiles.
706 */
707
708 injectPerForumStats(chart) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100709 waitFor(
710 () => {
711 let now = Date.now();
712 if (now - this.lastProfile.timestamp < 15 * 1000)
713 return Promise.resolve(this.lastProfile);
714 return Promise.reject(new Error(
715 'Didn\'t receive profile information (for per-profile stats)'));
716 },
717 {
718 interval: 500,
719 timeout: 15 * 1000,
720 })
721 .then(profile => {
722 const message = {
723 action: 'injectPerForumStatsSection',
724 prefix: 'TWPT-extrainfo',
725 profile: profile.body,
726 locale: this.displayLanguage,
727 };
728 window.postMessage(message, '*');
729 })
730 .catch(err => {
731 console.error(
732 'extraInfo: error while preparing to inject per-forum stats: ',
733 err);
734 });
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100735 }
736
737 injectPerForumStatsIfEnabled(chart) {
738 this.isEnabled('perforumstats').then(isEnabled => {
739 if (isEnabled) this.injectPerForumStats(chart);
740 });
741 }
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100742}