threadListAvatars: dynamic unauthorized forums

This is an implementation of point 3 in the "Idea" section of the
following doc: go/eu7T9m (public link available in the linked bug).

Paraphrasing the previous document:
This change adds code to maintain a list of forums from which the
extension couldn't load any thread, which probably correspond to private
forums. When a forum is in this list, avatars are no longer retrieved
from threads in that forum until the entry expires.

Bug: 2
Change-Id: I05e7b02818181f410855948e1af6ec75fc7c792b
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
index 3b27333..5e5eeee 100644
--- a/src/contentScripts/communityConsole/avatars.js
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -58,7 +58,9 @@
   // the thread belongs to a known private forum.
   shouldRetrieveAvatars(thread) {
     return this.getPrivateForums().then(privateForums => {
-      return !privateForums.includes(thread.forum);
+      if (privateForums.includes(thread.forum)) return false;
+
+      return this.db.isForumUnauthorized(thread.forum).then(res => !res);
     });
   }
 
@@ -90,8 +92,16 @@
                // 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 => {
+               /* authentication = */ false, /* authuser = */ 0,
+               /* returnUnauthorizedStatus = */ true)
+        .then(response => {
+          if (response.unauthorized)
+            return this.db.putUnauthorizedForum(thread.forum).then(() => {
+              throw new Error('Permission denied to load thread.');
+            });
+
+          var data = response.body;
+
           var numMessages = data?.['1']?.['8'];
           if (numMessages === undefined)
             throw new Error(
diff --git a/src/contentScripts/communityConsole/utils/AvatarsDB.js b/src/contentScripts/communityConsole/utils/AvatarsDB.js
index b59460d..a8c155c 100644
--- a/src/contentScripts/communityConsole/utils/AvatarsDB.js
+++ b/src/contentScripts/communityConsole/utils/AvatarsDB.js
@@ -3,11 +3,14 @@
 const dbName = 'TWPTAvatarsDB';
 const threadListLoadEvent = 'TWPT_ViewForumResponse';
 // Time after the last use when a cache entry should be deleted (in s):
-const expirationTime = 4 * 24 * 60 * 60;  // 4 days
+const cacheExpirationTime = 4 * 24 * 60 * 60;  // 4 days
 // Probability of running the piece of code to remove unused cache entries after
 // loading the thread list.
 const probRemoveUnusedCacheEntries = 0.10;  // 10%
 
+// Time after which an unauthorized forum entry expires (in s).
+const unauthorizedForumExpirationTime = 1 * 24 * 60 * 60;  // 1 day
+
 export default class AvatarsDB {
   constructor() {
     this.dbPromise = undefined;
@@ -38,6 +41,7 @@
       });
   }
 
+  // avatarsCache methods:
   getCacheEntry(threadId) {
     return this.dbPromise.then(db => db.get('avatarsCache', threadId));
   }
@@ -55,7 +59,7 @@
     return this.dbPromise
         .then(db => {
           var upperBoundTimestamp =
-              Math.floor(Date.now() / 1000) - expirationTime;
+              Math.floor(Date.now() / 1000) - cacheExpirationTime;
           var range = IDBKeyRange.upperBound(upperBoundTimestamp);
 
           var tx = db.transaction('avatarsCache', 'readwrite');
@@ -127,4 +131,30 @@
         });
     });
   }
+
+  // unauthorizedForums methods:
+  isForumUnauthorized(forumId) {
+    return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
+        .then(entry => {
+          if (entry === undefined) return false;
+
+          var now = Math.floor(Date.now() / 1000);
+          if (entry.expirationTimestamp > now) return true;
+
+          this.invalidateUnauthorizedForum(forumId);
+          return false;
+        });
+  }
+
+  putUnauthorizedForum(forumId) {
+    return this.dbPromise.then(db => db.put('unauthorizedForums', {
+      forumId,
+      expirationTimestamp:
+          Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
+    }));
+  }
+
+  invalidateUnauthorizedForum(forumId) {
+    return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
+  }
 };