blob: 873a62f2c744af3bd2fc85d1bd0d7c13a5e3aff4 [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
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010016// Used to match each category with the corresponding string.
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010017const kAbuseCategories = [
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010018 ['1', 'account'],
19 ['2', 'displayname'],
20 ['3', 'avatar'],
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010021];
22const kAbuseViolationCategories = {
23 0: 'NO_VIOLATION',
24 1: 'COMMUNITY_POLICY_VIOLATION',
25 2: 'LEGAL_VIOLATION',
26 3: 'CSAI_VIOLATION',
27 4: 'OTHER_VIOLATION',
28};
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +010029const kAbuseViolationCategoriesI18n = {
30 0: 'noviolation',
31 1: 'communitypolicy',
32 2: 'legal',
33 3: 'csai',
34 4: 'other',
35};
36
37// The following array will appear in the interface as is (without being
38// translated).
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +010039const kAbuseViolationTypes = {
40 0: 'UNSPECIFIED',
41 23: 'ACCOUNT_DISABLED',
42 55: 'ACCOUNT_HAS_SERVICES_DISABLED',
43 35: 'ACCOUNT_HIJACKED',
44 96: 'ACCOUNT_LEAKED_CREDENTIALS',
45 92: 'ACCOUNT_NOT_SUPPORTED',
46 81: 'ARTISTIC_NUDITY',
47 66: 'BAD_BEHAVIOR_PATTERN',
48 78: 'BAD_ENGAGEMENT_BEHAVIOR_PATTERN',
49 79: 'BORDERLINE_HARASSMENT',
50 80: 'BORDERLINE_HATE_SPEECH',
51 38: 'BOTNET',
52 32: 'BRANDING_VIOLATION',
53 100: 'CAPITALIZING_TRAGIC_EVENTS',
54 105: 'CLOAKING',
55 49: 'COIN_MINING',
56 7: 'COMMERCIAL_CONTENT',
57 97: 'COPPA_REGULATED',
58 57: 'COPYRIGHT_CIRCUMVENTION',
59 8: 'COPYRIGHTED_CONTENT',
60 58: 'COURT_ORDER',
61 51: 'CSAI',
62 94: 'CSAI_INSPECT',
63 52: 'CSAI_CARTOON_HUMOR',
64 53: 'CSAI_SOLICITATION',
65 108: 'CSAI_NON_APPARENT',
66 67: 'DANGEROUS',
67 37: 'DATA_SCRAPING',
68 86: 'DECEPTIVE_OAUTH_IMPLEMENTATION',
69 46: 'DEFAMATORY_CONTENT',
70 36: 'DELINQUENT_BILLING',
71 30: 'DISRUPTION_ATTEMPT',
72 112: 'DOMESTIC_INTERFERENCE',
73 22: 'DOS',
74 9: 'DUPLICATE_CONTENT',
75 68: 'DUPLICATE_LOCAL_PAGE',
76 121: 'NON_QUALIFYING_ORGANIZATION',
77 115: 'EGREGIOUS_INTERACTION_WITH_MINOR',
78 83: 'ENGAGEMENT_COLLUSION',
79 41: 'EXPLOIT_ATTACKS',
80 65: 'FAKE_USER',
81 2: 'FRAUD',
82 21: 'FREE_TRIAL_VIOLATION',
83 43: 'GIBBERISH',
84 101: 'FOREIGN_INTERFERENCE',
85 59: 'GOVERNMENT_ORDER',
86 10: 'GRAPHICAL_VIOLENCE',
87 11: 'HARASSMENT',
88 12: 'HATE_SPEECH',
89 90: 'IDENTICAL_PRODUCT_NAME',
90 60: 'ILLEGAL_DRUGS',
91 13: 'IMPERSONATION',
92 69: 'IMPERSONATION_WITH_PII',
93 116: 'INAPPROPRIATE_INTERACTION_WITH_MINOR',
94 45: 'INAPPROPRIATE_CONTENT_SPEECH',
95 106: 'INTENTIONAL_THWARTING',
96 27: 'INTRUSION_ATTEMPT',
97 87: 'INVALID_API_USAGE',
98 14: 'INVALID_CONTENT',
99 20: 'INVALID_GCE_USAGE',
100 120: 'INVALID_STORAGE_USAGE',
101 15: 'INVALID_IMAGE_QUALITY',
102 88: 'INVALID_API_PRIVACY_POLICY_DISCLOSURE',
103 54: 'INVALID_USAGE_OF_IP_PROXYING',
104 99: 'KEYWORD_STUFFING',
105 61: 'LEGAL_COUNTERFEIT',
106 62: 'LEGAL_EXPORT',
107 63: 'LEGAL_PRIVACY',
108 33: 'LEGAL_REVIEW',
109 91: 'LEGAL_PROTECTED',
110 70: 'LOW_QUALITY_CONTENT',
111 93: 'LOW_REPUTATION_PHONE_NUMBER',
112 6: 'MALICIOUS_SOFTWARE',
113 40: 'MALWARE',
114 113: 'MISLEADING',
115 114: 'MISREP_OF_ID',
116 89: 'MEMBER_OF_ABUSIVE_GCE_NETWORK',
117 84: 'NON_CONSENSUAL_EXPLICIT_IMAGERY',
118 1: 'NONE',
119 102: 'OFF_TOPIC',
120 31: 'OPEN_PROXY',
121 28: 'PAYMENT_FRAUD',
122 16: 'PEDOPHILIA',
123 71: 'PERSONAL_INFORMATION_CONTENT',
124 25: 'PHISHING',
125 34: 'POLICY_REVIEW',
126 17: 'PORNOGRAPHY',
127 29: 'QUOTA_CIRCUMVENTION',
128 72: 'QUOTA_EXCEEDED',
129 73: 'REGULATED',
130 24: 'REPEATED_POLICY_VIOLATION',
131 104: 'RESOURCE_COMPROMISED',
132 107: 'REWARD_PROGRAMS_ABUSE',
133 74: 'ROGUE_PHARMA',
134 82: 'ESCORT',
135 75: 'SPAMMY_LOCAL_VERTICAL',
136 39: 'SEND_EMAIL_SPAM',
137 117: 'SEXTORTION',
138 118: 'SEX_TRAFFICKING',
139 44: 'SEXUALLY_EXPLICIT_CONTENT',
140 3: 'SHARDING',
141 95: 'SOCIAL_ENGINEERING',
142 109: 'SUSPICIOUS',
143 19: 'TRADEMARK_CONTENT',
144 50: 'TRAFFIC_PUMPING',
145 76: 'UNSAFE_RACY',
146 103: 'UNUSUAL_ACTIVITY_ALERT',
147 64: 'UNWANTED_CONTENT',
148 26: 'UNWANTED_SOFTWARE',
149 77: 'VIOLENT_EXTREMISM',
150 119: 'UNAUTH_IMAGES_OF_MINORS',
151 85: 'UNAUTHORIZED_SERVICE_RESELLING',
152 98: 'CSAI_EXTERNAL',
153 5: 'SPAM',
154 4: 'UNSAFE',
155 47: 'CHILD_PORNOGRAPHY_INCITATION',
156 18: 'TERRORISM_SUPPORT',
157 56: 'CSAI_WORST_OF_WORST',
158};
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100159
160// These values will be translated
161const kItemMetadataStateI18n = {
162 1: 'published',
163 2: 'draft',
164 3: 'automated_abuse_take_down_hide',
165 4: 'automated_abuse_take_down_delete',
166 13: 'automated_abuse_reinstate',
167 10: 'automated_off_topic_hide',
168 14: 'automated_flagged_pending_manual_review',
169 5: 'user_flagged_pending_manual_review',
170 6: 'owner_deleted',
171 7: 'manual_take_down_hide',
172 17: 'manual_profile_take_down_suspend',
173 8: 'manual_take_down_delete',
174 18: 'reinstate_profile_takedown',
175 9: 'reinstate_abuse_takedown',
176 11: 'clear_off_topic',
177 12: 'confirm_off_topic',
178 15: 'googler_off_topic_hide',
179 16: 'expert_flagged_pending_manual_review',
180};
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100181const kItemMetadataState = {
182 0: 'UNDEFINED',
183 1: 'PUBLISHED',
184 2: 'DRAFT',
185 3: 'AUTOMATED_ABUSE_TAKE_DOWN_HIDE',
186 4: 'AUTOMATED_ABUSE_TAKE_DOWN_DELETE',
187 13: 'AUTOMATED_ABUSE_REINSTATE',
188 10: 'AUTOMATED_OFF_TOPIC_HIDE',
189 14: 'AUTOMATED_FLAGGED_PENDING_MANUAL_REVIEW',
190 5: 'USER_FLAGGED_PENDING_MANUAL_REVIEW',
191 6: 'OWNER_DELETED',
192 7: 'MANUAL_TAKE_DOWN_HIDE',
193 17: 'MANUAL_PROFILE_TAKE_DOWN_SUSPEND',
194 8: 'MANUAL_TAKE_DOWN_DELETE',
195 18: 'REINSTATE_PROFILE_TAKEDOWN',
196 9: 'REINSTATE_ABUSE_TAKEDOWN',
197 11: 'CLEAR_OFF_TOPIC',
198 12: 'CONFIRM_OFF_TOPIC',
199 15: 'GOOGLER_OFF_TOPIC_HIDE',
200 16: 'EXPERT_FLAGGED_PENDING_MANUAL_REVIEW',
201};
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100202
203export default class ExtraInfo {
204 constructor() {
205 this.lastProfile = {
206 body: {},
207 id: -1,
208 timestamp: 0,
209 };
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100210 this.lastCRsList = {
211 body: {},
212 id: -1,
213 duplicateNames: new Set(),
214 };
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100215 this.lastThread = {
216 body: {},
217 id: -1,
218 timestamp: 0,
219 };
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100220 this.lastThreadList = null;
221 this.lastThreadListTimestamp = 0;
222 this.lastThreadListRequestId = -1;
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100223 this.displayLanguage = getDisplayLanguage();
224 this.optionsWatcher = new OptionsWatcher(['extrainfo', 'perforumstats']);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100225 this.setUpHandlers();
226 }
227
228 setUpHandlers() {
229 window.addEventListener(kViewUnifiedUserResponseEvent, e => {
230 if (e.detail.id < this.lastProfile.id) return;
231
232 this.lastProfile = {
233 body: e.detail.body,
234 id: e.detail.id,
235 timestamp: Date.now(),
236 };
237 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100238 window.addEventListener(kListCannedResponsesResponse, e => {
239 if (e.detail.id < this.lastCRsList.id) return;
240
241 // Look if there are duplicate names
242 const crs = e.detail.body?.['1'] ?? [];
243 const names = crs.map(cr => cr?.['7']).slice().sort();
244 let duplicateNames = new Set();
245 for (let i = 1; i < names.length; i++)
246 if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
247
248 this.lastCRsList = {
249 body: e.detail.body,
250 id: e.detail.id,
251 duplicateNames,
252 };
253 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100254 window.addEventListener(kViewThreadResponse, e => {
255 if (e.detail.id < this.lastThread.id) return;
256
257 this.lastThread = {
258 body: e.detail.body,
259 id: e.detail.id,
260 timestamp: Date.now(),
261 };
262 });
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100263 window.addEventListener(kViewThreadResponse, e => {
264 if (e.detail.id < this.lastThread.id) return;
265
266 this.lastThread = {
267 body: e.detail.body,
268 id: e.detail.id,
269 timestamp: Date.now(),
270 };
271 });
272 window.addEventListener(kViewForumResponse, e => {
273 if (e.detail.id < this.lastThreadListRequestId) return;
274
275 this.lastThreadList = e.detail.body;
276 this.lastThreadListTimestamp = Date.now();
277 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100278 }
279
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100280 // Whether |feature| is enabled
281 isEnabled(feature) {
282 return this.optionsWatcher.isEnabled(feature);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100283 }
284
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100285 // Add chips which contain |info| to |node|. If |withContainer| is set to
286 // true, a container will contain all the chips.
287 addExtraInfoElement(info, node, withContainer = false) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100288 // Don't create if there's nothing to show
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100289 if (info.length == 0) return;
290
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100291 let container;
292 if (withContainer) {
293 container = document.createElement('div');
294 container.classList.add('TWPT-extrainfo-container');
295 } else {
296 container = node;
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100297 }
298
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100299 let tooltips = [];
300
301 for (const i of info) {
302 let chip = document.createElement('material-chip');
303 chip.classList.add('TWPT-extrainfo-chip');
304
305 let chipCont = document.createElement('div');
306 chipCont.classList.add('TWPT-chip-content-container');
307
308 let content = document.createElement('div');
309 content.classList.add('TWPT-content');
310
311 const [badge, badgeTooltip] = createExtBadge();
312
313 let span = document.createElement('span');
314 span.append(i);
315
316 content.append(badge, span);
317 chipCont.append(content);
318 chip.append(chipCont);
319 container.append(chip);
320
321 tooltips.push(badgeTooltip);
322 }
323
324 if (withContainer) node.append(container);
325
326 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100327 }
328
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100329 /**
330 * Profile functionality
331 */
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100332 injectAtProfile(card) {
333 waitFor(
334 () => {
335 let now = Date.now();
336 if (now - this.lastProfile.timestamp < 15 * 1000)
337 return Promise.resolve(this.lastProfile);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100338 return Promise.reject(
339 new Error('Didn\'t receive profile information'));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100340 },
341 {
342 interval: 500,
343 timeout: 15 * 1000,
344 })
345 .then(profile => {
346 let info = [];
347 const abuseViolationCategory = profile.body?.['1']?.['6'];
348 if (abuseViolationCategory) {
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100349 let avCat = document.createElement('span');
350 avCat.textContent = chrome.i18n.getMessage(
351 'inject_extrainfo_profile_abusecategory',
352 [chrome.i18n.getMessage(
353 'inject_extrainfo_profile_abusecategory_' +
354 kAbuseViolationCategoriesI18n[abuseViolationCategory]) ??
355 abuseViolationCategory]);
356 avCat.title = kAbuseViolationCategories[abuseViolationCategory] ??
357 abuseViolationCategory;
358 info.push(avCat);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100359 }
360
361 const profileAbuse = profile.body?.['1']?.['1']?.['8'];
362
363 for (const [index, category] of kAbuseCategories) {
364 const violation = profileAbuse?.[index]?.['1']?.['1'];
365 if (violation) {
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100366 info.push(chrome.i18n.getMessage(
367 'inject_extrainfo_profile_abuse_' + category,
368 [kAbuseViolationTypes[violation]]));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100369 }
370 }
371
372 const appealCount = profileAbuse?.['4'];
373 if (appealCount !== undefined)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100374 info.push(chrome.i18n.getMessage(
375 'inject_extrainfo_profile_appealsnum', [appealCount]));
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100376
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100377 this.addExtraInfoElement(info, card, true);
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100378 })
379 .catch(err => {
380 console.error(
381 'extraInfo: error while injecting profile extra info: ', err);
382 });
383 }
384
385 injectAtProfileIfEnabled(card) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100386 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100387 if (isEnabled) return this.injectAtProfile(card);
388 });
389 }
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100390
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100391 /**
392 * Canned responses (CRs) functionality
393 */
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100394 getCRName(tags, isExpanded) {
395 if (!isExpanded)
396 return tags.parentNode?.querySelector?.('.text .name')?.textContent;
397
398 // https://www.youtube.com/watch?v=Z6_ZNW1DACE
399 return tags.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
400 ?.parentNode?.parentNode?.querySelector?.('.text .name')
401 ?.textContent;
402 }
403
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100404 getUsageCountString(num) {
405 if (num == 1)
406 return chrome.i18n.getMessage('inject_extrainfo_crs_used_singular');
407
408 return chrome.i18n.getMessage('inject_extrainfo_crs_used_plural', [num]);
409 }
410
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100411 // Inject usage stats in the |tags| component of a CR
412 injectAtCR(tags, isExpanded) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100413 waitFor(
414 () => {
415 if (this.lastCRsList.id != -1)
416 return Promise.resolve(this.lastCRsList);
417 return Promise.reject(
418 new Error('Didn\'t receive canned responses list'));
419 },
420 {
421 interval: 500,
422 timeout: 15 * 1000,
423 })
424 .then(crs => {
425 let name = this.getCRName(tags, isExpanded);
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100426
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100427 // If another CR has the same name, there's no easy way to distinguish
428 // them, so don't show the usage stats.
429 if (crs.duplicateNames.has(name)) {
430 console.info(
431 'CR "' + name +
432 '" is duplicate, so skipping the injection of usage stats.');
433 return;
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100434 }
435
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100436 for (const cr of (crs.body?.['1'] ?? [])) {
437 if (cr['7'] == name) {
438 let tag = document.createElement('material-chip');
439 tag.classList.add('TWPT-tag');
440
441 let container = document.createElement('div');
442 container.classList.add('TWPT-chip-content-container');
443
444 let content = document.createElement('div');
445 content.classList.add('TWPT-content');
446
447 const [badge, badgeTooltip] = createExtBadge();
448
449 let label = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100450 label.textContent = this.getUsageCountString(cr['8'] ?? '0');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100451
452 content.append(badge, label);
453 container.append(content);
454 tag.append(container);
455 tags.append(tag);
456
457 new MDCTooltip(badgeTooltip);
458
459 if (cr['9']) {
460 const lastUsedTime = Math.floor(parseInt(cr['9']) / 1e3);
461 let date = (new Date(lastUsedTime)).toLocaleString();
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100462 createPlainTooltip(
463 label,
464 chrome.i18n.getMessage(
465 'inject_extrainfo_crs_lastused', [date]));
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100466 }
467
468 break;
469 }
470 }
471 })
472 .catch(err => {
473 console.error(
474 'extraInfo: error while injecting profile extra info: ', err);
475 });
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100476 }
477
478 injectAtCRIfEnabled(tags, isExpanded) {
479 // If the tag has already been injected, exit.
480 if (tags.querySelector('.TWPT-tag')) return;
481
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100482 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez74a25202022-01-23 23:36:58 +0100483 if (isEnabled) return this.injectAtCR(tags, isExpanded);
484 });
485 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100486
487 /**
488 * Thread view functionality
489 */
490
491 getPendingStateInfo(endPendingStateTimestampMicros) {
492 const endPendingStateTimestamp =
493 Math.floor(endPendingStateTimestampMicros / 1e3);
494 const now = Date.now();
495 if (endPendingStateTimestampMicros && endPendingStateTimestamp > now) {
496 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100497 span.textContent =
498 chrome.i18n.getMessage('inject_extrainfo_message_pendingstate');
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100499
500 let date = new Date(endPendingStateTimestamp).toLocaleString();
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100501 let pendingTooltip = createPlainTooltip(
502 span,
503 chrome.i18n.getMessage(
504 'inject_extrainfo_message_pendingstate_tooltip', [date]),
505 false);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100506 return [span, pendingTooltip];
507 }
508
509 return [null, null];
510 }
511
512 getMetadataInfo(itemMetadata) {
513 let info = [];
514
515 const state = itemMetadata?.['1'];
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100516 if (state && state != 1) {
517 let span = document.createElement('span');
518 span.textContent = chrome.i18n.getMessage(
519 'inject_extrainfo_message_state',
520 [chrome.i18n.getMessage(
521 'inject_extrainfo_message_state_' +
522 kItemMetadataStateI18n[state]) ??
523 state]);
524 span.title = kItemMetadataState[state] ?? state;
525 info.push(span);
526 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100527
528 const shadowBlockInfo = itemMetadata?.['10'];
529 const blockedTimestampMicros = shadowBlockInfo?.['2'];
530 if (blockedTimestampMicros) {
531 const isBlocked = shadowBlockInfo?.['1'];
532 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100533 span.textContent = chrome.i18n.getMessage(
534 'inject_extrainfo_message_shadowblock' +
535 (isBlocked ? 'active' : 'notactive'));
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100536 if (isBlocked) span.classList.add('TWPT-extrainfo-bad');
537 info.push(span);
538 }
539
540 return info;
541 }
542
543 getLiveReviewStatusInfo(liveReviewStatus) {
544 const verdict = liveReviewStatus?.['1'];
545 if (!verdict) return [null, null];
546 let label, labelClass;
547 switch (verdict) {
548 case 1: // LIVE_REVIEW_RELEVANT
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100549 label = 'relevant';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100550 labelClass = 'TWPT-extrainfo-good';
551 break;
552
553 case 2: // LIVE_REVIEW_OFF_TOPIC
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100554 label = 'offtopic';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100555 labelClass = 'TWPT-extrainfo-bad';
556 break;
557
558 case 3: // LIVE_REVIEW_ABUSE
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100559 label = 'abuse';
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100560 labelClass = 'TWPT-extrainfo-bad';
561 break;
562 }
563 const reviewedBy = liveReviewStatus?.['2'];
564 const timestamp = liveReviewStatus?.['3'];
565 const date = (new Date(Math.floor(timestamp / 1e3))).toLocaleString();
566
567 let a = document.createElement('a');
568 a.href = 'https://support.google.com/s/community/user/' + reviewedBy;
569 a.classList.add(labelClass);
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100570 a.textContent = chrome.i18n.getMessage(
571 'inject_extrainfo_message_livereviewverdict',
572 [chrome.i18n.getMessage(
573 'inject_extrainfo_message_livereviewverdict_' + label)]);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100574 let liveReviewTooltip = createPlainTooltip(a, date, false);
575 return [a, liveReviewTooltip];
576 }
577
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100578 // Get an |info| array with the info related to the thread, and a |tooltips|
579 // array with the corresponding tooltips which should be initialized after the
580 // info is added to the DOM.
581 //
582 // This is used by the injectAtQuestion() and injectAtThreadList() functions.
583 getThreadInfo(thread) {
584 let info = [];
585 let tooltips = [];
586
587 const endPendingStateTimestampMicros = thread?.['2']?.['39'];
588 const [pendingStateInfo, pendingTooltip] =
589 this.getPendingStateInfo(endPendingStateTimestampMicros);
590 if (pendingStateInfo) info.push(pendingStateInfo);
591 if (pendingTooltip) tooltips.push(pendingTooltip);
592
593 const isTrending = thread?.['2']?.['25'];
594 const isTrendingAutoMarked = thread?.['39'];
595 if (isTrendingAutoMarked)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100596 info.push(document.createTextNode(
597 chrome.i18n.getMessage('inject_extrainfo_thread_autotrending')));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100598 else if (isTrending)
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100599 info.push(document.createTextNode(
600 chrome.i18n.getMessage('inject_extrainfo_thread_trending')));
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100601
602 const itemMetadata = thread?.['2']?.['12'];
603 const mdInfo = this.getMetadataInfo(itemMetadata);
604 info.push(...mdInfo);
605
606 const liveReviewStatus = thread?.['2']?.['38'];
607 const [liveReviewInfo, liveReviewTooltip] =
608 this.getLiveReviewStatusInfo(liveReviewStatus);
609 if (liveReviewInfo) info.push(liveReviewInfo);
610 if (liveReviewTooltip) tooltips.push(liveReviewTooltip);
611
612 return [info, tooltips];
613 }
614
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100615 injectAtQuestion(stateChips) {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100616 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100617 if (currentPage === false) {
618 console.error('extraInfo: couldn\'t parse current URL:', location.href);
619 return;
620 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100621
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100622 waitFor(
623 () => {
624 let now = Date.now();
625 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
626 if (now - this.lastThread.timestamp < 30 * 1000 &&
627 threadInfo?.['1'] == currentPage.thread &&
628 threadInfo?.['3'] == currentPage.forum)
629 return Promise.resolve(this.lastThread);
630 return Promise.reject(
631 new Error('Didn\'t receive thread information'));
632 },
633 {
634 interval: 500,
635 timeout: 30 * 1000,
636 })
637 .then(thread => {
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100638 const [info, tooltips] = this.getThreadInfo(thread.body?.['1']);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100639 this.addExtraInfoElement(info, stateChips, false);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100640 for (const tooltip of tooltips) new MDCTooltip(tooltip);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100641 })
642 .catch(err => {
643 console.error(
644 'extraInfo: error while injecting question extra info: ', err);
645 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100646 }
647
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100648 injectAtQuestionIfEnabled(stateChips) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100649 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100650 if (isEnabled) return this.injectAtQuestion(stateChips);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100651 });
652 }
653
654 getMessagesByType(thread, type) {
655 if (type === 'reply') return thread?.['1']?.['3'];
656 if (type === 'lastMessage') return thread?.['1']?.['17']?.['3'];
657 if (type === 'suggested') return thread?.['1']?.['17']?.['4'];
658 if (type === 'recommended') return thread?.['1']?.['17']?.['1'];
659 }
660
661 getMessageByTypeAndIndex(thread, type, index) {
662 return this.getMessagesByType(thread, type)?.[index];
663 }
664
665 // Returns true if the last message is included in the messages array (in the
666 // extension context, we say those messages are of the type "reply").
667 lastMessageInReplies(thread) {
668 const lastMessageId = thread?.['1']?.['17']?.['3']?.[0]?.['1']?.['1'];
669 if (!lastMessageId) return true;
670
671 // If the last message is included in the lastMessage array, check if it
672 // also exists in the messages/replies array.
673 const replies = thread?.['1']?.['3'];
674 if (!replies?.length) return false;
675 const lastReplyIndex = replies.length - 1;
676 const lastReplyId = replies[lastReplyIndex]?.['1']?.['1'];
677 return lastMessageId && lastMessageId == lastReplyId;
678 }
679
680 getMessageInfo(thread, message) {
681 const section = message.parentNode;
682
683 let type = 'reply';
684 if (section?.querySelector?.('.heading material-icon[icon="auto_awesome"]'))
685 type = 'suggested';
686 if (section?.querySelector?.('.heading material-icon[icon="check_circle"]'))
687 type = 'recommended';
688
689 let index = -1;
690 let messagesInDom = section.querySelectorAll('ec-message');
691
692 // Number of messages in the DOM.
693 const n = messagesInDom.length;
694
695 if (type !== 'reply') {
696 for (let i = 0; i < n; ++i) {
697 if (message.isEqualNode(messagesInDom[i])) {
698 index = i;
699 break;
700 }
701 }
702 } else {
703 // If the type of the message is a reply, things are slightly more
704 // complex, since replies are paginated and the last message should be
705 // treated separately (it is included diferently in the API response).
706 let lastMessageInReplies = this.lastMessageInReplies(thread);
707 if (message.isEqualNode(messagesInDom[n - 1]) && !lastMessageInReplies) {
708 type = 'lastMessage';
709 index = 0
710 } else {
711 // Number of messages in the current API response.
712 const messagesInResponse = this.getMessagesByType(thread, type);
713 const m = messagesInResponse.length;
714 // If the last message is included in the replies array, we also have to
715 // consider the last message in the DOM.
716 let modifier = lastMessageInReplies ? 1 : 0;
717 for (let k = 0; k < m; ++k) {
718 let i = n - 2 - k + modifier;
719 if (message.isEqualNode(messagesInDom[i])) {
720 index = m - 1 - k;
721 break;
722 }
723 }
724 }
725 }
726
727 return [type, index];
728 }
729
730 injectAtMessage(messageNode) {
731 let currentPage = parseUrl(location.href);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100732 if (currentPage === false) {
733 console.error('extraInfo: couldn\'t parse current URL:', location.href);
734 return;
735 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100736
737 let footer = messageNode.querySelector('.footer-fill');
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100738 if (!footer) {
739 console.error('extraInfo: message doesn\'t have a footer:', messageNode);
740 return;
741 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100742
743 const [type, index] =
744 this.getMessageInfo(this.lastThread.body, messageNode);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100745 if (index == -1) {
746 console.error('extraInfo: this.getMessageInfo() returned index -1.');
747 return;
748 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100749
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100750 waitFor(
751 () => {
752 let now = Date.now();
753 let threadInfo = this.lastThread.body['1']?.['2']?.['1'];
754 if (now - this.lastThread.timestamp < 30 * 1000 &&
755 threadInfo?.['1'] == currentPage.thread &&
756 threadInfo?.['3'] == currentPage.forum) {
757 const message = this.getMessageByTypeAndIndex(
758 this.lastThread.body, type, index);
759 if (message) return Promise.resolve(message);
760 }
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100761
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100762 return Promise.reject(new Error(
763 'Didn\'t receive thread information (type: ' + type +
764 ', index: ' + index + ')'));
765 },
766 {
767 interval: 1000,
768 timeout: 30 * 1000,
769 })
770 .then(message => {
771 let info = [];
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100772
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100773 const endPendingStateTimestampMicros = message['1']?.['17'];
774 const [pendingStateInfo, pendingTooltip] =
775 this.getPendingStateInfo(endPendingStateTimestampMicros);
776 if (pendingStateInfo) info.push(pendingStateInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100777
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100778 const itemMetadata = message['1']?.['5'];
779 const mdInfo = this.getMetadataInfo(itemMetadata);
780 info.push(...mdInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100781
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100782 const liveReviewStatus = message['1']?.['36'];
783 const [liveReviewInfo, liveReviewTooltip] =
784 this.getLiveReviewStatusInfo(liveReviewStatus);
785 if (liveReviewInfo) info.push(liveReviewInfo);
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100786
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100787 this.addExtraInfoElement(info, footer, true);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100788 if (pendingTooltip) new MDCTooltip(pendingTooltip);
789 if (liveReviewTooltip) new MDCTooltip(liveReviewTooltip);
790 })
791 .catch(err => {
792 console.error(
793 'extraInfo: error while injecting message extra info: ', err);
794 });
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100795 }
796
797 injectAtMessageIfEnabled(message) {
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100798 this.isEnabled('extrainfo').then(isEnabled => {
Adrià Vilanova Martínez6e4a68d2022-01-24 21:44:32 +0100799 if (isEnabled) return this.injectAtMessage(message);
800 });
801 }
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100802
803 /**
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100804 * Thread list functionality
805 */
806 injectAtThreadList(li) {
807 waitFor(
808 () => {
809 const header = li.querySelector(
810 'ec-thread-summary .main-header .panel-description a.header');
811 if (header === null) {
812 console.error(
813 'extraInfo: Header is not present in the thread item\'s DOM.');
814 return;
815 }
816
817 const threadInfo = parseUrl(header.href);
818 if (threadInfo === false) {
819 console.error('extraInfo: Thread\'s link cannot be parsed.');
820 return;
821 }
822
823 let authorLine = li.querySelector(
824 'ec-thread-summary .header-content .top-row .author-line');
825 if (!authorLine) {
826 console.error(
827 'extraInfo: Author line is not present in the thread item\'s DOM.');
828 return;
829 }
830
831 let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
832 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
833 t?.['2']?.['1']?.['3'] == threadInfo.forum;
834 });
835 if (thread) return Promise.resolve([thread, authorLine]);
836 return Promise.reject(
837 new Error('Didn\'t receive thread information'));
838 },
839 {
840 interval: 500,
841 timeout: 7 * 1000,
842 })
843 .then(response => {
844 const [thread, authorLine] = response;
845 const state = thread?.['2']?.['12']?.['1'];
846 if (state && ![1, 13, 18, 9].includes(state)) {
847 let label = document.createElement('div');
848 label.classList.add('TWPT-label');
849
850 const [badge, badgeTooltip] = createExtBadge();
851
852 let span = document.createElement('span');
Adrià Vilanova Martínez7492d842022-02-08 00:33:14 +0100853 span.textContent = chrome.i18n.getMessage(
854 'inject_extrainfo_message_state_' +
855 kItemMetadataStateI18n[state]) ??
856 state;
857 span.title = kItemMetadataState[state] ?? state;
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100858
859 label.append(badge, span);
860 authorLine.prepend(label);
861 new MDCTooltip(badgeTooltip);
862 }
863 })
864 .catch(err => {
865 console.error(
866 'extraInfo: error while injecting thread list extra info: ', err);
867 });
868 }
869
870 injectAtThreadListIfEnabled(li) {
871 this.isEnabled('extrainfo').then(isEnabled => {
872 if (isEnabled) this.injectAtThreadList(li);
873 });
874 }
875
876 injectAtExpandedThreadList(toolbelt) {
877 const header =
878 toolbelt?.parentNode?.parentNode?.parentNode?.querySelector?.(
879 '.main-header .panel-description a.header');
880 if (header === null) {
881 console.error(
882 'extraInfo: Header is not present in the thread item\'s DOM.');
883 return;
884 }
885
886 const threadInfo = parseUrl(header.href);
887 if (threadInfo === false) {
888 console.error('extraInfo: Thread\'s link cannot be parsed.');
889 return;
890 }
891
892 waitFor(
893 () => {
894 let thread = this.lastThreadList?.['1']?.['2']?.find?.(t => {
895 return t?.['2']?.['1']?.['1'] == threadInfo.thread &&
896 t?.['2']?.['1']?.['3'] == threadInfo.forum;
897 });
898 if (thread) return Promise.resolve(thread);
899 return Promise.reject(
900 new Error('Didn\'t receive thread information'));
901 },
902 {
903 interval: 500,
904 timeout: 7 * 1000,
905 })
906 .then(thread => {
907 const [info, tooltips] = this.getThreadInfo(thread);
Adrià Vilanova Martínez09b3bdb2022-02-05 00:15:05 +0100908 this.addExtraInfoElement(info, toolbelt, true);
Adrià Vilanova Martínez854cb912022-02-02 16:18:23 +0100909 for (const tooltip of tooltips) new MDCTooltip(tooltip);
910 })
911 .catch(err => {
912 console.error(
913 'extraInfo: error while injecting thread list extra info: ', err);
914 });
915 }
916
917 injectAtExpandedThreadListIfEnabled(toolbelt) {
918 this.isEnabled('extrainfo').then(isEnabled => {
919 if (isEnabled) this.injectAtExpandedThreadList(toolbelt);
920 });
921 }
922
923 /**
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100924 * Per-forum stats in user profiles.
925 */
926
927 injectPerForumStats(chart) {
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100928 waitFor(
929 () => {
930 let now = Date.now();
931 if (now - this.lastProfile.timestamp < 15 * 1000)
932 return Promise.resolve(this.lastProfile);
933 return Promise.reject(new Error(
934 'Didn\'t receive profile information (for per-profile stats)'));
935 },
936 {
937 interval: 500,
938 timeout: 15 * 1000,
939 })
940 .then(profile => {
Adrià Vilanova Martínez69c30502022-01-28 20:47:08 +0100941 new PerForumStatsSection(
942 chart?.parentNode, profile.body, this.displayLanguage);
Adrià Vilanova Martínez102041d2022-01-28 18:39:29 +0100943 })
944 .catch(err => {
945 console.error(
946 'extraInfo: error while preparing to inject per-forum stats: ',
947 err);
948 });
Adrià Vilanova Martínez4f56d562022-01-26 00:23:27 +0100949 }
950
951 injectPerForumStatsIfEnabled(chart) {
952 this.isEnabled('perforumstats').then(isEnabled => {
953 if (isEnabled) this.injectPerForumStats(chart);
954 });
955 }
Adrià Vilanova Martínez7e8796c2022-01-23 21:46:46 +0100956}