threadListAvatars: skip calls to load private threads
- Refactor threadListAvatars code.
- Skip calls to load the avatars for threads in known private forums
(the private forum ids are retrieved from the startup data object.
- Change order of CCApi method parameters (the authuser is not needed
when |authentication| is |false|). Also, now the |authenticated|
parameter is no longer optional. This is to make it explicit in the
code and prevent mistakes in the future.
Change-Id: Ie47c85dcf00ffbfe269721e5f565ba5dd5259b3b
diff --git a/src/common/api.js b/src/common/api.js
index 17c0211..20a6c7d 100644
--- a/src/common/api.js
+++ b/src/common/api.js
@@ -2,7 +2,7 @@
// Function to wrap calls to the Community Console API with intelligent error
// handling.
-function CCApi(method, data, authuser, authenticated = true) {
+function CCApi(method, data, authenticated, authuser = 0) {
var authuserPart =
authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser);
diff --git a/src/content_scripts/console_inject.js b/src/content_scripts/console_inject.js
index 837c2ae..d501c26 100644
--- a/src/content_scripts/console_inject.js
+++ b/src/content_scripts/console_inject.js
@@ -242,92 +242,165 @@
clone, (readToggle.nextSibling || readToggle));
}
-// TODO(avm99963): This is a prototype. DON'T FORGET TO ADD ERROR HANDLING.
-function injectAvatars(node) {
- var header = node.querySelector(
- 'ec-thread-summary .main-header .panel-description a.header');
- if (header === null) return;
+var avatars = {
+ isFilterSetUp: false,
+ privateForums: [],
- var link = parseUrl(header.href);
- if (link === false) return;
+ // Gets a list of private forums. If it is already cached, the cached list is
+ // returned; otherwise it is also computed and cached.
+ getPrivateForums() {
+ return new Promise((resolve, reject) => {
+ if (this.isFilterSetUp) return resolve(this.privateForums);
- var APIRequestUrl = 'https://support.google.com/s/community/api/ViewThread' +
- (authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser));
+ if (!document.documentElement.hasAttribute('data-startup'))
+ return reject('[threadListAvatars] Couldn\'t get startup data.');
- fetch(APIRequestUrl, {
- 'headers': {
- 'content-type': 'text/plain; charset=utf-8',
- },
- 'body': JSON.stringify({
- 1: link.forum,
- 2: link.thread,
- 3: {
- 1: {2: 15},
- 3: true,
- 5: true,
- 10: true,
- 16: true,
- 18: true,
+ var startupData =
+ JSON.parse(document.documentElement.getAttribute('data-startup'));
+ var forums = startupData?.['1']?.['2'];
+ if (forums === undefined)
+ return reject(
+ '[threadListAvatars] Couldn\'t retrieve forums from startup data.');
+
+ for (var f of forums) {
+ var forumId = f?.['2']?.['1']?.['1'];
+ var forumVisibility = f?.['2']?.['18'];
+ if (forumId === undefined || forumVisibility === undefined) {
+ console.warn(
+ '[threadListAvatars] Coudln\'t retrieve forum id and/or forum visibility for the following forum:',
+ f);
+ continue;
+ }
+
+ // forumVisibility's value 1 means "PUBLIC".
+ if (forumVisibility != 1) this.privateForums.push(forumId);
}
- }),
- 'method': 'POST',
- 'mode': 'cors',
- 'credentials': 'omit',
- })
- .then(res => {
- if (res.status == 200 || res.status == 400) {
- return res.json().then(data => ({
- status: res.status,
- body: data,
- }));
- } else {
- throw new Error('Status code ' + res.status + ' was not expected.');
- }
- })
- .then(res => {
- if (res.status == 400) {
- throw new Error(
- res.body[4] ||
- ('Response status: 400. Error code: ' + res.body[2]));
- }
- return res.body;
- })
- .then(data => {
- if (!('1' in data) || !('8' in data['1'])) return false;
+ // Forum 51488989 is marked as public but it is in fact private.
+ this.privateForums.push('51488989');
- var messages = data['1']['8'];
- if (messages == 0) return;
+ this.isFilterSetUp = true;
+ return resolve(this.privateForums);
+ });
+ },
+ // Some threads belong to private forums, and this feature will not be able to
+ // get its avatars since it makes an anonymomus call to get the contents of
+ // the thread.
+ //
+ // This function returns whether avatars should be retrieved depending on if
+ // the thread belongs to a known private forum.
+ shouldRetrieveAvatars(thread) {
+ return this.getPrivateForums().then(privateForums => {
+ return !privateForums.includes(thread.forum);
+ });
+ },
+
+ // Get an array with the first |num| messages from the thread |thread|.
+ getFirstMessages(thread, num = 15) {
+ return CCApi(
+ 'ViewThread', {
+ 1: thread.forum,
+ 2: thread.thread,
+ // options
+ 3: {
+ // pagination
+ 1: {
+ 2: num, // maxNum
+ },
+ 3: true, // withMessages
+ 5: true, // withUserProfile
+ 10: false, // withPromotedMessages
+ 16: false, // withThreadNotes
+ 18: true, // sendNewThreadIfMoved
+ }
+ },
+ // |authentication| is false because otherwise this would mark
+ // the thread as read as a side effect, and that would mark all
+ // threads in the list as read.
+ //
+ // Due to the fact that we have to call this endpoint
+ // anonymously, this means we can't retrieve information about
+ // threads in private forums.
+ /* authentication = */ false)
+ .then(data => {
+ var numMessages = data?.['1']?.['8'];
+ if (numMessages == undefined)
+ throw new Error(
+ 'Request to view thread doesn\'t include the number of messages');
+ if (numMessages == 0) return [];
+
+ var messages = data?.['1']['3'];
+ if (messages == undefined)
+ throw new Error(
+ 'numMessages was ' + numMessages +
+ ' but the response didn\'t include any message.');
+ return messages;
+ });
+ },
+
+ // Get a list of at most |num| avatars for thread |thread|
+ getVisibleAvatars(thread, num = 3) {
+ return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
+ if (!shouldRetrieve) {
+ console.debug('[threadListAvatars] Skipping thread', thread);
+ return [];
+ }
+
+ return this.getFirstMessages(thread).then(messages => {
var avatarUrls = [];
+ for (var m of messages) {
+ var url = m?.['3']?.['1']?.['2'];
- if (!('3' in data['1'])) return false;
- for (var m of data['1']['3']) {
- if (!('3' in m) || !('1' in m['3']) || !('2' in m['3']['1']))
- continue;
-
- var url = m['3']['1']['2'];
-
+ if (url === undefined) continue;
if (!avatarUrls.includes(url)) avatarUrls.push(url);
-
if (avatarUrls.length == 3) break;
}
- var avatarsContainer = document.createElement('div');
- avatarsContainer.classList.add('TWPT-avatars');
-
- var count = Math.floor(Math.random() * 4);
-
- for (var i = 0; i < avatarUrls.length; ++i) {
- var avatar = document.createElement('div');
- avatar.classList.add('TWPT-avatar');
- avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
- avatarsContainer.appendChild(avatar);
- }
-
- header.appendChild(avatarsContainer);
+ return avatarUrls;
});
-}
+ });
+ },
+
+ // Inject avatars for thread summary (thread item) |node| in a thread list.
+ inject(node) {
+ var header = node.querySelector(
+ 'ec-thread-summary .main-header .panel-description a.header');
+ if (header === null) {
+ console.error(
+ '[threadListAvatars] Header is not present in the thread item\'s DOM.');
+ return;
+ }
+
+ var thread = parseUrl(header.href);
+ if (thread === false) {
+ console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
+ return;
+ }
+
+ this.getVisibleAvatars(thread)
+ .then(avatarUrls => {
+ var avatarsContainer = document.createElement('div');
+ avatarsContainer.classList.add('TWPT-avatars');
+
+ var count = Math.floor(Math.random() * 4);
+
+ for (var i = 0; i < avatarUrls.length; ++i) {
+ var avatar = document.createElement('div');
+ avatar.classList.add('TWPT-avatar');
+ avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
+ avatarsContainer.appendChild(avatar);
+ }
+
+ header.appendChild(avatarsContainer);
+ })
+ .catch(err => {
+ console.error(
+ '[threadListAvatars] Could not retrieve avatars for thread',
+ thread, err);
+ });
+ },
+};
var autoRefresh = {
isLookingForUpdates: false,
@@ -670,7 +743,7 @@
11: false, // withExpertResponder
},
},
- authuser)
+ true, authuser)
.then(thread => {
if (thread?.[1]?.[6] === true) {
console.debug(
@@ -693,7 +766,7 @@
3: lastMessageId,
}],
},
- authuser);
+ true, authuser);
})
.then(_ => {
console.debug(
@@ -817,7 +890,7 @@
if (options.threadlistavatars && ('tagName' in node) &&
(node.tagName == 'LI') &&
node.querySelector('ec-thread-summary') !== null) {
- injectAvatars(node);
+ avatars.inject(node);
}
// Set up the autorefresh list feature