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