Add initial extra info feature
Only the extra profile in profiles is shown, since it is the easiest to
implement.
Bug: twpowertools:93
Change-Id: Ife4f31ee056f74bd478702347d75044b024abf1e
diff --git a/src/common/optionsPrototype.json5 b/src/common/optionsPrototype.json5
index ca38042..0d337b2 100644
--- a/src/common/optionsPrototype.json5
+++ b/src/common/optionsPrototype.json5
@@ -126,6 +126,11 @@
context: 'experiments',
killSwitchType: 'experiment',
},
+ 'extrainfo': {
+ defaultValue: false,
+ context: 'experiments',
+ killSwitchType: 'experiment',
+ },
// Internal options:
'ccdarktheme_switch_enabled': {
diff --git a/src/common/xhrInterceptors.json5 b/src/common/xhrInterceptors.json5
index 63bec85..2e9a443 100644
--- a/src/common/xhrInterceptors.json5
+++ b/src/common/xhrInterceptors.json5
@@ -15,5 +15,10 @@
urlRegex: "api/CreateMessage",
intercepts: "request",
},
+ {
+ eventName: "ViewUnifiedUserResponse",
+ urlRegex: "api/ViewUnifiedUser",
+ intercepts: "response",
+ },
],
}
diff --git a/src/contentScripts/communityConsole/extraInfo.js b/src/contentScripts/communityConsole/extraInfo.js
new file mode 100644
index 0000000..c572523
--- /dev/null
+++ b/src/contentScripts/communityConsole/extraInfo.js
@@ -0,0 +1,257 @@
+import {MDCTooltip} from '@material/tooltip';
+import {waitFor} from 'poll-until-promise';
+
+import {isOptionEnabled} from '../../common/optionsUtils.js';
+
+import {createExtBadge} from './utils/common.js';
+
+const kViewUnifiedUserResponseEvent = 'TWPT_ViewUnifiedUserResponse';
+
+const kAbuseCategories = [
+ ['1', 'Account'],
+ ['2', 'Display name'],
+ ['3', 'Avatar'],
+];
+const kAbuseViolationCategories = {
+ 0: 'NO_VIOLATION',
+ 1: 'COMMUNITY_POLICY_VIOLATION',
+ 2: 'LEGAL_VIOLATION',
+ 3: 'CSAI_VIOLATION',
+ 4: 'OTHER_VIOLATION',
+};
+const kAbuseViolationTypes = {
+ 0: 'UNSPECIFIED',
+ 23: 'ACCOUNT_DISABLED',
+ 55: 'ACCOUNT_HAS_SERVICES_DISABLED',
+ 35: 'ACCOUNT_HIJACKED',
+ 96: 'ACCOUNT_LEAKED_CREDENTIALS',
+ 92: 'ACCOUNT_NOT_SUPPORTED',
+ 81: 'ARTISTIC_NUDITY',
+ 66: 'BAD_BEHAVIOR_PATTERN',
+ 78: 'BAD_ENGAGEMENT_BEHAVIOR_PATTERN',
+ 79: 'BORDERLINE_HARASSMENT',
+ 80: 'BORDERLINE_HATE_SPEECH',
+ 38: 'BOTNET',
+ 32: 'BRANDING_VIOLATION',
+ 100: 'CAPITALIZING_TRAGIC_EVENTS',
+ 105: 'CLOAKING',
+ 49: 'COIN_MINING',
+ 7: 'COMMERCIAL_CONTENT',
+ 97: 'COPPA_REGULATED',
+ 57: 'COPYRIGHT_CIRCUMVENTION',
+ 8: 'COPYRIGHTED_CONTENT',
+ 58: 'COURT_ORDER',
+ 51: 'CSAI',
+ 94: 'CSAI_INSPECT',
+ 52: 'CSAI_CARTOON_HUMOR',
+ 53: 'CSAI_SOLICITATION',
+ 108: 'CSAI_NON_APPARENT',
+ 67: 'DANGEROUS',
+ 37: 'DATA_SCRAPING',
+ 86: 'DECEPTIVE_OAUTH_IMPLEMENTATION',
+ 46: 'DEFAMATORY_CONTENT',
+ 36: 'DELINQUENT_BILLING',
+ 30: 'DISRUPTION_ATTEMPT',
+ 112: 'DOMESTIC_INTERFERENCE',
+ 22: 'DOS',
+ 9: 'DUPLICATE_CONTENT',
+ 68: 'DUPLICATE_LOCAL_PAGE',
+ 121: 'NON_QUALIFYING_ORGANIZATION',
+ 115: 'EGREGIOUS_INTERACTION_WITH_MINOR',
+ 83: 'ENGAGEMENT_COLLUSION',
+ 41: 'EXPLOIT_ATTACKS',
+ 65: 'FAKE_USER',
+ 2: 'FRAUD',
+ 21: 'FREE_TRIAL_VIOLATION',
+ 43: 'GIBBERISH',
+ 101: 'FOREIGN_INTERFERENCE',
+ 59: 'GOVERNMENT_ORDER',
+ 10: 'GRAPHICAL_VIOLENCE',
+ 11: 'HARASSMENT',
+ 12: 'HATE_SPEECH',
+ 90: 'IDENTICAL_PRODUCT_NAME',
+ 60: 'ILLEGAL_DRUGS',
+ 13: 'IMPERSONATION',
+ 69: 'IMPERSONATION_WITH_PII',
+ 116: 'INAPPROPRIATE_INTERACTION_WITH_MINOR',
+ 45: 'INAPPROPRIATE_CONTENT_SPEECH',
+ 106: 'INTENTIONAL_THWARTING',
+ 27: 'INTRUSION_ATTEMPT',
+ 87: 'INVALID_API_USAGE',
+ 14: 'INVALID_CONTENT',
+ 20: 'INVALID_GCE_USAGE',
+ 120: 'INVALID_STORAGE_USAGE',
+ 15: 'INVALID_IMAGE_QUALITY',
+ 88: 'INVALID_API_PRIVACY_POLICY_DISCLOSURE',
+ 54: 'INVALID_USAGE_OF_IP_PROXYING',
+ 99: 'KEYWORD_STUFFING',
+ 61: 'LEGAL_COUNTERFEIT',
+ 62: 'LEGAL_EXPORT',
+ 63: 'LEGAL_PRIVACY',
+ 33: 'LEGAL_REVIEW',
+ 91: 'LEGAL_PROTECTED',
+ 70: 'LOW_QUALITY_CONTENT',
+ 93: 'LOW_REPUTATION_PHONE_NUMBER',
+ 6: 'MALICIOUS_SOFTWARE',
+ 40: 'MALWARE',
+ 113: 'MISLEADING',
+ 114: 'MISREP_OF_ID',
+ 89: 'MEMBER_OF_ABUSIVE_GCE_NETWORK',
+ 84: 'NON_CONSENSUAL_EXPLICIT_IMAGERY',
+ 1: 'NONE',
+ 102: 'OFF_TOPIC',
+ 31: 'OPEN_PROXY',
+ 28: 'PAYMENT_FRAUD',
+ 16: 'PEDOPHILIA',
+ 71: 'PERSONAL_INFORMATION_CONTENT',
+ 25: 'PHISHING',
+ 34: 'POLICY_REVIEW',
+ 17: 'PORNOGRAPHY',
+ 29: 'QUOTA_CIRCUMVENTION',
+ 72: 'QUOTA_EXCEEDED',
+ 73: 'REGULATED',
+ 24: 'REPEATED_POLICY_VIOLATION',
+ 104: 'RESOURCE_COMPROMISED',
+ 107: 'REWARD_PROGRAMS_ABUSE',
+ 74: 'ROGUE_PHARMA',
+ 82: 'ESCORT',
+ 75: 'SPAMMY_LOCAL_VERTICAL',
+ 39: 'SEND_EMAIL_SPAM',
+ 117: 'SEXTORTION',
+ 118: 'SEX_TRAFFICKING',
+ 44: 'SEXUALLY_EXPLICIT_CONTENT',
+ 3: 'SHARDING',
+ 95: 'SOCIAL_ENGINEERING',
+ 109: 'SUSPICIOUS',
+ 19: 'TRADEMARK_CONTENT',
+ 50: 'TRAFFIC_PUMPING',
+ 76: 'UNSAFE_RACY',
+ 103: 'UNUSUAL_ACTIVITY_ALERT',
+ 64: 'UNWANTED_CONTENT',
+ 26: 'UNWANTED_SOFTWARE',
+ 77: 'VIOLENT_EXTREMISM',
+ 119: 'UNAUTH_IMAGES_OF_MINORS',
+ 85: 'UNAUTHORIZED_SERVICE_RESELLING',
+ 98: 'CSAI_EXTERNAL',
+ 5: 'SPAM',
+ 4: 'UNSAFE',
+ 47: 'CHILD_PORNOGRAPHY_INCITATION',
+ 18: 'TERRORISM_SUPPORT',
+ 56: 'CSAI_WORST_OF_WORST',
+};
+
+export default class ExtraInfo {
+ constructor() {
+ this.lastProfile = {
+ body: {},
+ id: -1,
+ timestamp: 0,
+ };
+ this.setUpHandlers();
+ }
+
+ setUpHandlers() {
+ window.addEventListener(kViewUnifiedUserResponseEvent, e => {
+ if (e.detail.id < this.lastProfile.id) return;
+
+ this.lastProfile = {
+ body: e.detail.body,
+ id: e.detail.id,
+ timestamp: Date.now(),
+ };
+ });
+ }
+
+ // Add a pretty component which contains |info| to |node|.
+ addExtraInfoElement(info, node) {
+ // Don't create
+ if (info.length == 0) return;
+
+ let container = document.createElement('div');
+ container.classList.add('TWPT-extrainfo-container');
+
+ let badgeCell = document.createElement('div');
+ badgeCell.classList.add('TWPT-extrainfo-badge-cell');
+
+ let badge, badgeTooltip;
+ [badge, badgeTooltip] = createExtBadge();
+ badgeCell.append(badge);
+
+ let infoCell = document.createElement('div');
+ infoCell.classList.add('TWPT-extrainfo-info-cell');
+
+ for (const i of info) {
+ let iRow = document.createElement('div');
+ iRow.append(i);
+ infoCell.append(iRow);
+ }
+
+ container.append(badgeCell, infoCell);
+ node.append(container);
+ new MDCTooltip(badgeTooltip);
+ }
+
+ fieldInfo(field, value) {
+ let span = document.createElement('span');
+ span.append(document.createTextNode(field + ': '));
+
+ let valueEl = document.createElement('span');
+ valueEl.style.fontFamily = 'monospace';
+ valueEl.textContent = value;
+
+ span.append(valueEl);
+ return span;
+ }
+
+ // Profile functionality
+ injectAtProfile(card) {
+ waitFor(
+ () => {
+ let now = Date.now();
+ if (now - this.lastProfile.timestamp < 15 * 1000)
+ return Promise.resolve(this.lastProfile);
+ return Promise.reject('Didn\'t receive profile information');
+ },
+ {
+ interval: 500,
+ timeout: 15 * 1000,
+ })
+ .then(profile => {
+ let info = [];
+ const abuseViolationCategory = profile.body?.['1']?.['6'];
+ if (abuseViolationCategory) {
+ info.push(this.fieldInfo(
+ 'Abuse category',
+ kAbuseViolationCategories[abuseViolationCategory] ??
+ abuseViolationCategory));
+ }
+
+ const profileAbuse = profile.body?.['1']?.['1']?.['8'];
+
+ for (const [index, category] of kAbuseCategories) {
+ const violation = profileAbuse?.[index]?.['1']?.['1'];
+ if (violation) {
+ info.push(this.fieldInfo(
+ category + ' policy violation',
+ kAbuseViolationTypes[violation]));
+ }
+ }
+
+ const appealCount = profileAbuse?.['4'];
+ if (appealCount !== undefined)
+ info.push(this.fieldInfo('Number of appeals', appealCount));
+
+ this.addExtraInfoElement(info, card);
+ })
+ .catch(err => {
+ console.error(
+ 'extraInfo: error while injecting profile extra info: ', err);
+ });
+ }
+
+ injectAtProfileIfEnabled(card) {
+ isOptionEnabled('extrainfo').then(isEnabled => {
+ if (isEnabled) return this.injectAtProfile(card);
+ });
+ }
+}
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 870f1c4..a534f38 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -22,6 +22,9 @@
// Load more bar (for the "load more"/"load all" buttons)
'.load-more-bar',
+ // User profile card inside ec-unified-user
+ 'ec-unified-user .scTailwindUser_profileUsercardmain',
+
// Username span/editor inside ec-unified-user (user profile view)
'ec-unified-user .scTailwindUser_profileUsercarddetails',
@@ -89,6 +92,11 @@
}
}
+ // Show additional details in the profile view.
+ if (node.matches('ec-unified-user .scTailwindUser_profileUsercardmain')) {
+ window.TWPTExtraInfo.injectAtProfileIfEnabled(node);
+ }
+
// Show the "previous posts" links if the option is currently enabled.
// Here we're selecting the 'ec-user > div' element (unique child)
if (node.matches(
@@ -116,8 +124,7 @@
// Inject the batch lock and workflow buttons in the thread list if the
// corresponding options are currently enabled.
// The order is the inverse because the first one will be shown last.
- if (batchLock.shouldAddButton(node))
- batchLock.addButtonIfEnabled(node);
+ if (batchLock.shouldAddButton(node)) batchLock.addButtonIfEnabled(node);
if (workflows.shouldAddThreadListBtn(node))
workflows.addThreadListBtnIfEnabled(node);
@@ -241,4 +248,6 @@
injectStylesheet(chrome.runtime.getURL('css/thread_list_avatars.css'));
// Auto refresh list
injectStylesheet(chrome.runtime.getURL('css/autorefresh_list.css'));
+ // Extra info
+ injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
});
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index 4e8af62..8b1c4ef 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -2,6 +2,7 @@
import {getOptions} from '../../common/optionsUtils.js';
import AutoRefresh from './autoRefresh.js';
+import ExtraInfo from './extraInfo.js';
getOptions(null).then(options => {
/* IMPORTANT NOTE: Remember to change this when changing the "ifs" below!! */
@@ -17,9 +18,10 @@
'data-startup', JSON.stringify(startup));
}
- // Initialized here instead of in main.js so the first |ViewForumResponse|
- // event is received if it happens when the page loads.
+ // Initialized here instead of in main.js so the first event is received if it
+ // happens when the page loads.
window.TWPTAutoRefresh = new AutoRefresh();
+ window.TWPTExtraInfo = new ExtraInfo();
if (options.ccdarktheme) {
switch (options.ccdarktheme_mode) {
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index 1c3e75b..2637808 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -151,6 +151,10 @@
"message": "Manage workflows",
"description": "Button in the options page which opens the workflow management page."
},
+ "options_extrainfo": {
+ "message": "Show extra information in threads and profiles.",
+ "description": "Feature checkbox in the options page"
+ },
"options_save": {
"message": "Save",
"description": "Button in the options page to save the settings"
diff --git a/src/static/css/extrainfo.css b/src/static/css/extrainfo.css
new file mode 100644
index 0000000..86eed01
--- /dev/null
+++ b/src/static/css/extrainfo.css
@@ -0,0 +1,34 @@
+.TWPT-extrainfo-container {
+ display: inline-flex;
+ color: var(--TWPT-interop-secondary-text, #444746)!important;
+ align-items: center;
+}
+
+.TWPT-extrainfo-badge-cell {
+ margin: 0 8px;
+ --icon-size: 14px;
+ opacity: 0.6;
+}
+
+.TWPT-extrainfo-info-cell {
+ border-left: solid 1px var(--TWPT-interop-subtle-border, #ababab);
+ padding-left: 8px;
+ display: flex;
+ flex-direction: column;
+}
+
+.TWPT-extrainfo-info-cell > div:not(:last-child) {
+ margin-bottom: 2px;
+}
+
+/* Specific styles inside context */
+
+ec-unified-user .scTailwindUser_profileUsercardmain {
+ display: flex;
+ flex-direction: column;
+}
+
+ec-unified-user .TWPT-extrainfo-container {
+ margin-top: 16px;
+ align-self: end;
+}
diff --git a/src/static/options/experiments.html b/src/static/options/experiments.html
index 83a58cb..9a21fdf 100644
--- a/src/static/options/experiments.html
+++ b/src/static/options/experiments.html
@@ -14,6 +14,7 @@
<form>
<div id="optional-permissions-warning" hidden data-i18n="optionalpermissionswarning_header"></div>
<div class="option"><input type="checkbox" id="workflows"> <label for="workflows" data-i18n="workflows"></label> <button id="manage-workflows" data-i18n="workflows_manage"></button></div>
+ <div class="option"><input type="checkbox" id="extrainfo"> <label for="extrainfo" data-i18n="extrainfo"></label></div>
<div class="actions"><button id="save" data-i18n="save"></button></div>
</form>
<div id="save-indicator"></div>
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 800f2ae..1b2e8d6 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -79,7 +79,8 @@
"css/reposition_expand_thread.css",
"css/thread_list_avatars.css",
"css/autorefresh_list.css",
- "css/image_max_height.css"
+ "css/image_max_height.css",
+ "css/extrainfo.css"
#if defined(CHROMIUM_MV3)
],
"matches": [